Compare commits

..

10 Commits

Author SHA1 Message Date
2dff948e00 wip: share module
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-21 12:37:22 +02:00
1606ff5937 chore: use go 1.20.2
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 19:27:05 +02:00
90020d6ea6 feat(module,cast): enhance casting device discovery speed
Some checks failed
arcad/edge/pipeline/head There was a failure building this commit
2023-04-20 19:20:52 +02:00
7b6e39088d feat(cli,run): use 127.0.0.1 as default host value if empty
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 14:17:03 +02:00
9944a37670 chore: cache modd tool
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 13:57:46 +02:00
78307b6850 feat(cli,app): filter out ipv6 adresses for url resolving 2023-04-20 13:57:46 +02:00
2543386e5c Mise à jour de 'doc/apps/client-api/edge-menu.md'
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 13:14:12 +02:00
20c4189599 feat(sdk,client): minify generated script
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 10:41:03 +02:00
c7b639b643 feat(cli,app): compress http responses
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 10:36:20 +02:00
b5b4042cc7 feat(sdk,client): add menu to help navigation between apps
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 10:17:37 +02:00
94 changed files with 4223 additions and 4681 deletions

1
.env.dist Normal file
View File

@ -0,0 +1 @@
RUN_APP_ARGS=""

View File

@ -9,11 +9,14 @@ ESBUILD_VERSION ?= v0.17.5
GIT_VERSION := $(shell git describe --always)
DATE_VERSION := $(shell date +%Y.%-m.%-d)
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
APP_PATH ?= misc/client-sdk-testsuite/dist
RUN_APP_ARGS ?=
SHELL := bash
build: build-edge-cli build-client-sdk-test-app
watch:
go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest
watch: tools/modd/bin/modd
tools/modd/bin/modd
.PHONY: test
test: test-go
@ -48,6 +51,7 @@ 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 \
--minify \
--bundle \
--sourcemap \
--target=es2015 \
@ -55,12 +59,18 @@ pkg/sdk/client/dist/client.js: tools/esbuild/bin/esbuild node_modules
--global-name=Edge \
--define:global=window \
--platform=browser \
--footer:js="EdgeFrame=Edge.crossFrameMessenger;Edge=Edge.client" \
--loader:.svg=dataurl \
--outfile=pkg/sdk/client/dist/client.js
node_modules:
npm ci
run-app: .env
( set -o allexport && source .env && set +o allexport && bin/cli app run -p $(APP_PATH) $$RUN_APP_ARGS )
.env:
cp .env.dist .env
gitea-release: tools/yq/bin/yq tools/gitea-release/bin/gitea-release.sh build
mkdir -p .gitea-release
rm -rf .gitea-release/*
@ -92,3 +102,7 @@ tools/yq/bin/yq:
mkdir -p tools/yq/bin
curl -L --output tools/yq/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_amd64
chmod +x tools/yq/bin/yq
tools/modd/bin/modd:
mkdir -p tools/modd/bin
GOBIN=$(PWD)/tools/modd/bin go install -mod=readonly github.com/cortesi/modd/cmd/modd@latest

View File

@ -4,9 +4,9 @@
"algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$cWOxfEyBy4EyKZR5usB2Pw$xG+Z/E2DUJP9kF0s1fhZjIuP03gFQ65dP7pHRJz7eR8",
"claims": {
"arcad_entrypoint": "edge",
"arcad_role": "superadmin",
"arcad_tenant": "dev.cli",
"edge_entrypoint": "edge",
"edge_role": "superadmin",
"edge_tenant": "dev.cli",
"preferred_username": "SuperAdmin",
"sub": "superadmin"
}
@ -16,9 +16,9 @@
"algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$WXXc4ECnkej6WO7f0Xya6Q$UG2wcGltJcuW0cNTR85mAl65tI1kFWMMw7ADS2FMOvY",
"claims": {
"arcad_entrypoint": "edge",
"arcad_role": "admin",
"arcad_tenant": "dev.cli",
"edge_entrypoint": "edge",
"edge_role": "admin",
"edge_tenant": "dev.cli",
"preferred_username": "Admin",
"sub": "admin"
}
@ -28,9 +28,9 @@
"algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$gkDAWCzfU23+un3x0ny+YA$L/NSPrd5iKPK/UnSCKfSz4EO+v94N3LTLky4QGJOfpI",
"claims": {
"arcad_entrypoint": "edge",
"arcad_role": "superuser",
"arcad_tenant": "dev.cli",
"edge_entrypoint": "edge",
"edge_role": "superuser",
"edge_tenant": "dev.cli",
"preferred_username": "SuperUser",
"sub": "superuser"
}
@ -40,9 +40,9 @@
"algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$DhUm9qXUKP35Lzp5M37eZA$2+h6yDxSTHZqFZIuI7JZfFWozwrObna8a8yCgEEPlPE",
"claims": {
"arcad_entrypoint": "edge",
"arcad_role": "user",
"arcad_tenant": "dev.cli",
"edge_entrypoint": "edge",
"edge_role": "user",
"edge_tenant": "dev.cli",
"preferred_username": "User",
"sub": "user"
}

View File

@ -9,7 +9,9 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
@ -18,18 +20,19 @@ import (
"forge.cadoles.com/arcad/edge/pkg/module"
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
"forge.cadoles.com/arcad/edge/pkg/module/blob"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"forge.cadoles.com/arcad/edge/pkg/module/fetch"
netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
shareModule "forge.cadoles.com/arcad/edge/pkg/module/share"
shareSqlite "forge.cadoles.com/arcad/edge/pkg/module/share/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
storageSqlite "forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/bundle"
"github.com/dop251/goja"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/lestrrat-go/jwx/v2/jwa"
@ -48,15 +51,15 @@ func RunCommand() *cli.Command {
Name: "run",
Usage: "Run the specified app bundle",
Flags: []cli.Flag{
&cli.StringFlag{
&cli.StringSliceFlag{
Name: "path",
Usage: "use `PATH` as app bundle (zipped bundle or directory)",
Aliases: []string{"p"},
Value: ".",
Value: cli.NewStringSlice("."),
},
&cli.StringFlag{
Name: "address",
Usage: "use `ADDRESS` as http server listening address",
Usage: "use `ADDRESS` as http server base listening address",
Aliases: []string{"a"},
Value: ":8080",
},
@ -75,6 +78,11 @@ func RunCommand() *cli.Command {
Usage: "use `FILE` for SQLite storage database",
Value: ".edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
},
&cli.StringFlag{
Name: "shared-resources-file",
Usage: "use `FILE` for SQLite shared resources database",
Value: ".edge/shared-resources.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
},
&cli.StringFlag{
Name: "accounts-file",
Usage: "use `FILE` as local accounts",
@ -83,22 +91,78 @@ func RunCommand() *cli.Command {
},
Action: func(ctx *cli.Context) error {
address := ctx.String("address")
path := ctx.String("path")
paths := ctx.StringSlice("path")
logFormat := ctx.String("log-format")
logLevel := ctx.Int("log-level")
storageFile := ctx.String("storage-file")
accountsFile := ctx.String("accounts-file")
sharedResourcesFile := ctx.String("shared-resources-file")
logger.SetFormat(logger.Format(logFormat))
logger.SetLevel(logger.Level(logLevel))
cmdCtx := ctx.Context
host, portStr, err := net.SplitHostPort(address)
if err != nil {
return errors.WithStack(err)
}
port, err := strconv.ParseUint(portStr, 10, 32)
if err != nil {
return errors.WithStack(err)
}
manifests := make([]*app.Manifest, len(paths))
for idx, pth := range paths {
bdl, err := bundle.FromPath(pth)
if err != nil {
return errors.WithStack(err)
}
manifest, err := app.LoadManifest(bdl)
if err != nil {
return errors.WithStack(err)
}
manifests[idx] = manifest
}
var wg sync.WaitGroup
for idx, p := range paths {
wg.Add(1)
go func(path string, basePort uint64, appIndex int) {
defer wg.Done()
port := basePort + uint64(appIndex)
address := fmt.Sprintf("%s:%d", host, port)
appsRepository := newAppRepository(host, basePort, manifests...)
appCtx := logger.With(cmdCtx, logger.F("address", address))
if err := runApp(appCtx, path, address, storageFile, accountsFile, appsRepository, sharedResourcesFile); err != nil {
logger.Error(appCtx, "could not run app", logger.E(errors.WithStack(err)))
}
}(p, port, idx)
}
wg.Wait()
return nil
},
}
}
func runApp(ctx context.Context, path string, address string, storageFile string, accountsFile string, appRepository appModule.Repository, sharedResourcesFile string) error {
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))
logger.Info(ctx, "opening app bundle", logger.F("path", absPath))
bundle, err := bundle.FromPath(path)
if err != nil {
@ -114,24 +178,43 @@ func RunCommand() *cli.Command {
return errors.Wrap(err, "invalid app manifest")
}
storageFile := injectAppID(ctx.String("storage-file"), manifest.ID)
ctx = logger.With(ctx, logger.F("appID", manifest.ID))
if err := ensureDir(storageFile); err != nil {
return errors.WithStack(err)
}
db, err := sqlite.Open(storageFile)
// Add auth handler
key, err := dummyKey()
if err != nil {
return errors.WithStack(err)
}
ds := sqlite.NewDocumentStoreWithDB(db)
bs := sqlite.NewBlobStoreWithDB(db)
bus := memory.NewBus()
deps := &moduleDeps{}
funcs := []ModuleDepFunc{
initMemoryBus,
initDatastores(storageFile, manifest.ID),
initAccounts(accountsFile, manifest.ID),
initShareRepository(sharedResourcesFile),
initAppRepository(appRepository),
}
for _, fn := range funcs {
if err := fn(deps); err != nil {
return errors.WithStack(err)
}
}
handler := appHTTP.NewHandler(
appHTTP.WithBus(bus),
appHTTP.WithServerModules(getServerModules(bus, ds, bs, manifest, address)...),
appHTTP.WithBus(deps.Bus),
appHTTP.WithServerModules(getServerModules(deps)...),
appHTTP.WithHTTPMounts(
appModule.Mount(appRepository),
authModule.Mount(
authHTTP.NewLocalHandler(
jwa.HS256, key,
authHTTP.WithRoutePrefix("/auth"),
authHTTP.WithAccounts(deps.Accounts...),
),
authModule.WithJWT(dummyKeySet),
),
),
)
if err := handler.Load(bundle); err != nil {
return errors.Wrap(err, "could not load app bundle")
@ -139,93 +222,48 @@ func RunCommand() *cli.Command {
router := chi.NewRouter()
router.Use(middleware.Logger)
accountsFile := injectAppID(ctx.String("accounts-file"), manifest.ID)
accounts, err := loadLocalAccounts(accountsFile)
if err != nil {
return errors.Wrap(err, "could not load local accounts")
}
// Add auth handler
key, err := dummyKey()
if err != nil {
return errors.WithStack(err)
}
router.Handle("/auth/*", authHTTP.NewLocalHandler(
jwa.HS256, key,
authHTTP.WithRoutePrefix("/auth"),
authHTTP.WithAccounts(accounts...),
))
router.Use(middleware.Compress(5))
// Add app handler
router.Handle("/*", handler)
logger.Info(cmdCtx, "listening", logger.F("address", address))
logger.Info(ctx, "listening", logger.F("address", address))
if err := http.ListenAndServe(address, router); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore, manifest *app.Manifest, address string) []app.ServerModuleFactory {
type moduleDeps struct {
AppID app.ID
Bus bus.Bus
DocumentStore storage.DocumentStore
BlobStore storage.BlobStore
AppRepository appModule.Repository
ShareRepository shareModule.Repository
Accounts []authHTTP.LocalAccount
}
type ModuleDepFunc func(*moduleDeps) error
func getServerModules(deps *moduleDeps) []app.ServerModuleFactory {
return []app.ServerModuleFactory{
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
cast.CastModuleFactory(),
module.LifecycleModuleFactory(),
netModule.ModuleFactory(bus),
module.RPCModuleFactory(bus),
module.StoreModuleFactory(ds),
blob.ModuleFactory(bus, bs),
module.Extends(
auth.ModuleFactory(
auth.WithJWT(dummyKeySet),
netModule.ModuleFactory(deps.Bus),
module.RPCModuleFactory(deps.Bus),
module.StoreModuleFactory(deps.DocumentStore),
blob.ModuleFactory(deps.Bus, deps.BlobStore),
authModule.ModuleFactory(
authModule.WithJWT(dummyKeySet),
),
func(o *goja.Object) {
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
panic(errors.New("could not set 'CLAIM_TENANT' property"))
}
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
}
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
panic(errors.New("could not set 'CLAIM_ROLE' property"))
}
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
}
},
),
appModule.ModuleFactory(appModuleMemory.NewRepository(
func(ctx context.Context, id app.ID, from string) (string, error) {
addr := address
if strings.HasPrefix(addr, ":") {
addr = "0.0.0.0" + addr
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
return "", errors.WithStack(err)
}
addr, err = findMatchingDeviceAddress(ctx, from, host)
if err != nil {
return "", errors.WithStack(err)
}
return fmt.Sprintf("http://%s:%s", addr, port), nil
},
manifest,
)),
fetch.ModuleFactory(bus),
appModule.ModuleFactory(deps.AppRepository),
fetch.ModuleFactory(deps.Bus),
shareModule.ModuleFactory(deps.AppID, deps.ShareRepository),
}
}
@ -343,9 +381,101 @@ func findMatchingDeviceAddress(ctx context.Context, from string, defaultAddr str
continue
}
return ip.String(), nil
if ip.To4() == nil {
continue
}
return ip.To4().String(), nil
}
}
return defaultAddr, nil
}
func newAppRepository(host string, basePort uint64, manifests ...*app.Manifest) *appModuleMemory.Repository {
if host == "" {
host = "127.0.0.1"
}
return appModuleMemory.NewRepository(
func(ctx context.Context, id app.ID, from string) (string, error) {
appIndex := 0
for i := 0; i < len(manifests); i++ {
if manifests[i].ID == id {
appIndex = i
break
}
}
addr, err := findMatchingDeviceAddress(ctx, from, host)
if err != nil {
return "", errors.WithStack(err)
}
return fmt.Sprintf("http://%s:%d", addr, int(basePort)+appIndex), nil
},
manifests...,
)
}
func initAppRepository(repo appModule.Repository) ModuleDepFunc {
return func(deps *moduleDeps) error {
deps.AppRepository = repo
return nil
}
}
func initMemoryBus(deps *moduleDeps) error {
deps.Bus = memory.NewBus()
return nil
}
func initDatastores(storageFile string, appID app.ID) ModuleDepFunc {
return func(deps *moduleDeps) error {
storageFile = injectAppID(storageFile, appID)
if err := ensureDir(storageFile); err != nil {
return errors.WithStack(err)
}
db, err := storageSqlite.Open(storageFile)
if err != nil {
return errors.WithStack(err)
}
deps.DocumentStore = storageSqlite.NewDocumentStoreWithDB(db)
deps.BlobStore = storageSqlite.NewBlobStoreWithDB(db)
return nil
}
}
func initAccounts(accountsFile string, appID app.ID) ModuleDepFunc {
return func(deps *moduleDeps) error {
accountsFile = injectAppID(accountsFile, appID)
accounts, err := loadLocalAccounts(accountsFile)
if err != nil {
return errors.Wrap(err, "could not load local accounts")
}
deps.Accounts = accounts
return nil
}
}
func initShareRepository(shareRepositoryFile string) ModuleDepFunc {
return func(deps *moduleDeps) error {
if err := ensureDir(shareRepositoryFile); err != nil {
return errors.WithStack(err)
}
repo := shareSqlite.NewRepository(shareRepositoryFile)
deps.ShareRepository = repo
return nil
}
}

View File

@ -5,7 +5,7 @@ import (
"log"
"time"
"github.com/barnybug/go-cast/discovery"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
@ -14,21 +14,26 @@ func ScanCommand() *cli.Command {
return &cli.Command{
Name: "scan",
Usage: "Scan network for casting devices",
Flags: []cli.Flag{},
Flags: []cli.Flag{
&cli.DurationFlag{
Name: "timeout",
Aliases: []string{"t"},
Value: 30 * time.Second,
},
},
Action: func(ctx *cli.Context) error {
service := discovery.NewService(ctx.Context)
defer service.Stop()
timeout := ctx.Duration("timeout")
go func() {
if err := service.Run(ctx.Context, time.Second); err != nil && !errors.Is(err, context.DeadlineExceeded) {
searchCtx, cancel := context.WithTimeout(ctx.Context, timeout)
defer cancel()
devices, err := cast.SearchDevices(searchCtx)
if err != nil {
log.Fatalf("%+v", errors.WithStack(err))
}
}()
found := service.Found()
for device := range found {
log.Printf("[DEVICE] %s %s %s:%d", device.Uuid(), device.Name(), device.IP().String(), device.Port())
for dev := range devices {
log.Printf("[DEVICE] %s %s %s:%d", dev.UUID, dev.Name, dev.Host.String(), dev.Port)
}
return nil

View File

@ -10,5 +10,6 @@ Afin de pouvoir utiliser le SDK "client", vous devez inclure dans la page HTML d
Vous pourrez ensuite accéder aux variables globales suivantes:
- [`Edge`](./edge.md) - Client principal d'échange avec le serveur
- [`EdgeFrame`](./edge-frame.md)
- [`Edge.Client`](./edge-client.md) - Client principal d'échange avec le serveur
- [`Edge.Frame`](./edge-frame.md) - Utilitaire de communication avec une frame parente
- [`Edge.Menu`](./edge-menu.md) - Gestionnaire de menu

View File

@ -1,22 +1,22 @@
# `Edge`
# `Edge.Client`
## Méthodes
### `Edge.connect(): Promise`
### `Edge.Client.connect(): Promise`
> `TODO`
### `Edge.disconnect(): void`
### `Edge.Client.disconnect(): void`
> `TODO`
### `Edge.send(message: Object): void`
### `Edge.Client.send(message: Object): void`
> `TODO`
### `Edge.rpc(method: string, params: Object): Promise`
### `Edge.Client.rpc(method: string, params: Object): Promise`
> `TODO`
#### Exemple
@ -36,22 +36,22 @@ function echo(ctx, params) {
**Côté client**
```js
Edge.connect().then(() => {
Edge.rpc("echo", { hello: "world!" })
Edge.Client.connect().then(() => {
Edge.Client.rpc("echo", { hello: "world!" })
.then(result => console.log(result))
.catch(err => console.error(err));
});
```
### `Edge.upload(blob: Blob, metadata: Object): Promise`
### `Edge.Client.upload(blob: Blob, metadata: Object): Promise`
> `TODO`
### `Edge.blobUrl(bucketName: string, blobId: string): string`
### `Edge.Client.blobUrl(bucketName: string, blobId: string): string`
> `TODO`
### `Edge.externalUrl(url: string): string`
### `Edge.Client.externalUrl(url: string): string`
Retourne une URL "locale" permettant d'accéder à une ressource externe, en fonction de règles propres à l'application. Voir module [`fetch`](../server-api/fetch.md).
@ -64,5 +64,5 @@ Retourne une URL "locale" permettant d'accéder à une ressource externe, en fon
#### Exemple
```js
Edge.addEventListener("message", evt => console.log(evt.detail));
Edge.Client.addEventListener("message", evt => console.log(evt.detail));
```

View File

@ -1,8 +1,8 @@
# `EdgeFrame`
# `Edge.Frame`
## Méthodes
### `EdgeFrame.addEventListener(name: string, listener: (event) => void)`
### `Edge.Frame.addEventListener(name: string, listener: (event) => void)`
> `TODO`

View File

@ -0,0 +1,27 @@
# `Edge.Menu`
## Méthodes
### `Edge.Menu.show()`
Afficher le menu.
### `Edge.Menu.hide()`
Cacher le menu.
### `Edge.Menu.setItem(name: string, label:string, options?: { iconUrl?: string, linkUrl?: string, order?: number })`
Créer/mettre à jour l'item nommé de la section du menu associée à l'application.
### `Edge.Menu.removeItem(name: string)`
Supprimer l'item de la section du menu associée à l'application.
### `Edge.Menu.setAppIconUrl(url: string)`
Mettre à jour l'URL de l'icône de la section du menu associée à l'application.
### `Edge.Menu.setAppTitle(title: string)`
Mettre à jour le titre de la section du menu associée à l'application.

View File

@ -40,13 +40,13 @@ Créer le fichier `my-app/public/index.html`:
<script type="text/javascript">
// On utilise le SDK via la variable globale "Edge"
// pour se connecter au serveur de notre application.
Edge.connect().then(() => {
Edge.Client.connect().then(() => {
// Une fois connecté, on envoie un message au serveur.
Edge.send({ "hello": "world" });
Edge.Client.send({ "hello": "world" });
});
// On écoute les messages en provenance du serveur.
Edge.addEventListener("message", (evt) => {
Edge.Client.addEventListener("message", (evt) => {
console.log("New server message", evt.detail)
});
</script>

View File

@ -29,4 +29,5 @@ Listes des modules disponibles côté serveur.
- [`fetch`](./fetch.md)
- [`net`](./net.md)
- [`rpc`](./rpc.md)
- [`share`](./share.md)
- [`store`](./store.md)

View File

@ -86,4 +86,4 @@ interface BlobInfo {
### `Metadata`
L'objet `Metadata` est un objet clé/valeur arbitraire transmis avec la requête de téléversement. Voir la méthode [`Edge.upload(blob, metadata)`](../client-api/README.md#edge-upload-blob-blob-metadata-object-promise) du SDK client.
L'objet `Metadata` est un objet clé/valeur arbitraire transmis avec la requête de téléversement. Voir la méthode [`Edge.Client.upload(blob, metadata)`](../client-api/README.md#edge-upload-blob-blob-metadata-object-promise) du SDK client.

View File

@ -13,7 +13,7 @@ Pour permettre aux utilisateurs d'accéder à des ressources distantes, vous dev
**Côté client**
```js
// Création d'une URL "locale" permettant d'accéder à la ressource distante
var url = Edge.externalUrl("http://example.com")
var url = Edge.Client.externalUrl("http://example.com")
// Vous pouvez utiliser l'URL comme attribut `src` d'une balise <img> par exemple
// ou effectuer une requête fetch() avec celle ci.

View File

@ -32,9 +32,9 @@ Aucune
```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.Client.on('message', evt => console.log(evt.detail));
Edge.connect();
Edge.Client.connect();
```
**Côté serveur**

View File

@ -1,6 +1,6 @@
# 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).
Ce module permet de déclarer des méthodes côté serveur qui seront "invoquable" côté client via la méthode [`Edge.Client.rpc(method: string, params: Object): Promise`](../client-api/README.md#edgerpcmethod-string-params-object-promise).
## Méthodes
@ -31,8 +31,8 @@ function echo(ctx, params) {
**Côté client**
```js
Edge.connect().then(() => {
Edge.rpc("echo", { hello: "world!" })
Edge.Client.connect().then(() => {
Edge.Client.rpc("echo", { hello: "world!" })
.then(result => console.log(result))
.catch(err => console.error(err));
});

View File

@ -0,0 +1,143 @@
# Module `share`
Ce module permet partager des ressources à destination des autres applications exécutées dans l'environnement Edge.
## Propriétés
### `share.ANY_TYPE`, `share.ANY_NAME`
Les propriétés `share.ANY_TYPE` et `share.ANY_NAME` sont utilisées dans la méthode `share.findResources()` pour récupérer ne pas appliquer de filtre spécifique au type ou au nom des attributs respectivement.
### `share.TYPE_TEXT`, `share.TYPE_NUMBER`, `share.TYPE_PATH`, `share.TYPE_BOOL`
Ces propriétés correspondant aux types d'attributs possibles dans une ressource.
Le type `share.TYPE_PATH` décrit un "chemin" destiné à être transformé en URL par l'application consommatrice de la ressource sous la forme `${APP_URL}${PATH}`. Ce type d'attribut peut être utilisé pour partager des URLs (médias, pages, etc) entre applications.
## Méthodes
### `share.upsertResource(ctx: Context, resourceId: string, ...attributes: Attribute[]): Resource`
Cette méthode permet de créer une ressource ou de la mettre à jour si elle existe déjà. Elle prend en paramètre le contexte d'exécution, l'identifiant de la ressource et une liste d'attributs.
Si la ressource n'existe pas, elle sera créée avec les attributs fournis. Si elle existe, les attributs existants seront mis à jour avec les valeurs fournies.
#### Arguments
- `ctx: Context`: Le contexte d'exécution.
- `resourceId: string`: L'identifiant de la ressource.
- `...attributes: Attribute[]`: Une liste d'attributs. Chaque attribut est représenté par un objet de type `Attribute`.
#### Valeur de retour
La méthode retourne un objet de type `Resource` qui représente la ressource créée ou mise à jour.
#### Usage
```javascript
const resource = share.upsertResource(ctx, "my-resource", { name: "color", type: share.TYPE_TEXT, value: "red" });
console.log(resource);
// Output: { id: "my-resource", origin: "my.app", attributes: [{ name: "color", type: "text", value: "red", createdAt: "2023-04-21T14:30:00Z", updatedAt: "2023-04-21T14:30:00Z" }] }
```
### `share.findResources(ctx: Context, withAttribute?: string, withType?: string): []Resource`
Cette méthode permet de rechercher des ressources en fonction de leurs attributs. Elle prend en paramètre le contexte d'exécution et deux paramètres optionnels qui permettent de filtrer les ressources.
#### Arguments
- `ctx: Context`: Le contexte d'exécution.
- `withAttribute?: string`: (optionnel) Le nom de l'attribut à rechercher (`share.ANY_NAME` par défaut)
- `withType?: string`: (optionnel) Le type de l'attribut à rechercher (`share.ANY_TYPE` par défaut)
#### Valeur de retour
La méthode retourne un tableau d'objets de type `Resource` qui représentent les ressources correspondant aux critères de recherche.
#### Usage
```typescript
const resources = share.findResources(ctx, "color", share.TYPE_TEXT);
console.log(resources);
// Output: [{ id: "my-resource", origin: "my/app", attributes: [{ name: "color", type: "text", value: "red", createdAt: "2023-04-21T14:30:00Z", updatedAt: "2023-04-21T14:30:00Z" }] }]
```
### `share.deleteAttributes(ctx: Context, resourceId: string, ...names: string[]): Resource`
Cette méthode supprime un ou plusieurs attributs de la ressource spécifiée.
#### Arguments
- `ctx: Context`: contexte d'exécution
- `resourceId: string`: identifiant unique de la ressource à modifier
- `...names: string[]`: tableau de noms d'attributs à supprimer
#### Valeur de retour
La méthode retourne un objet de type `Resource` qui représente la ressource modifiée.
#### Usage
```typescript
const resource = share.upsertResource(ctx, "my-resource", { name: "color", type: share.TYPE_TEXT, value: "red" });
console.log(resource);
// Output: { id: "my-resource", origin: "my.app", attributes: [{ name: "color", type: "text", value: "red", createdAt: "2023-04-21T14:30:00Z", updatedAt: "2023-04-21T14:30:00Z" }] }
```
### `share.deleteResource(ctx: Context, resourceId: string)`
Cette méthode supprime la ressource spécifiée.
#### Arguments
- `ctx: Context`: contexte d'exécution
- `resourceId: string`: identifiant unique de la ressource à supprimer
#### Valeur de retour
La méthode ne retourne pas de valeur.
#### Usage
```typescript
const resource = share.deleteResource(ctx, "my-resource");
```
## Objets
### `Context`
Voir la documentation du module [`context`](./context.md)
### `Resource`
```typescript
interface Resource {
id: string
origin: string
attributes: Attribute[]
}
```
### `Attribute`
```typescript
interface Attribute {
name: string
type: ValueType
createdAt: string
updatedAt: string
}
```
### `ValueType`
```typescript
enum ValueType {
TYPE_TEXT = "text",
TYPE_PATH = "path",
TYPE_NUMBER = "number",
TYPE_BOOL = "bool"
}
```

35
go.mod
View File

@ -3,33 +3,37 @@ module forge.cadoles.com/arcad/edge
go 1.19
require (
github.com/hashicorp/mdns v1.0.5
github.com/lestrrat-go/jwx/v2 v2.0.8
modernc.org/sqlite v1.20.4
)
require (
github.com/brutella/dnssd v1.2.6 // indirect
cloud.google.com/go v0.75.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/go-playground/locales v0.12.1 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 // indirect
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 // indirect
github.com/leodido/go-urn v1.1.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/miekg/dns v1.1.53 // indirect
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 // indirect
google.golang.org/grpc v1.35.0 // indirect
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
)
require (
cdr.dev/slog v1.4.0 // indirect
cdr.dev/slog v1.4.0
github.com/alecthomas/chroma v0.7.0 // indirect
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692
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/davecgh/go-spew v1.1.1
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097
@ -51,18 +55,17 @@ require (
github.com/pkg/errors v0.9.1
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
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
go.opencensus.io v0.22.5 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.org/x/crypto v0.7.0
golang.org/x/mod v0.10.0
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.8.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.4.0
lukechampine.com/uint128 v1.2.0 // indirect

120
go.sum
View File

@ -5,7 +5,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
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=
@ -18,7 +17,7 @@ cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZ
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 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY=
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=
@ -37,25 +36,25 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
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 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
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 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
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 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 h1:JW4WZlqyaNWUUahfr7MigeDW6jmtam5cTzzo1lwsFhE=
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692/go.mod h1:Au0ipPuCBA7zsOC61SnyrYetm8VT3vo1UJtwHeYke44=
github.com/brutella/dnssd v1.2.6 h1:/0P13JkHLRzeLQkWRPEn4hJCr4T3NfknIFw3aNPIC34=
github.com/brutella/dnssd v1.2.6/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
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=
@ -78,7 +77,6 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
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=
@ -109,7 +107,9 @@ github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL
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 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
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 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
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=
@ -117,8 +117,6 @@ github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e h1:eeyMpoxANuWNQ9O2auv4wXxJsrXzLUhdHaOmNWEGkRY=
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
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=
@ -145,6 +143,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
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 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
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=
@ -158,6 +157,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
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/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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=
@ -169,8 +169,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
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/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
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=
@ -178,7 +178,6 @@ 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=
@ -186,13 +185,13 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
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/go.net v0.0.0-20151006203346-104dcad90073 h1:9dodOMuH6u7LvPEkVydBv6KTHdm+SqsHOxHTzRW+1+w=
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
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/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 h1:yupxZNIxm5U8Tfb8g65irIuHkgF8c4koHC7daPSyMTE=
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8/go.mod h1:aa76Av3qgPeIQp9Y3qIkTBPieQYNkQ13Kxe7pze9Wb0=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
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=
@ -203,13 +202,15 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
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 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
@ -228,14 +229,14 @@ github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaa
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/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 h1:ALvJ9V8nNf04PFHMR2sot56N/pjrx5LzZGvUlnhdiCE=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
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=
@ -250,19 +251,19 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
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 v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
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 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -272,9 +273,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
@ -287,10 +288,9 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
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=
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b h1:nkvOl8TCj/mErADnwFFynjxBtC+hHsrESw6rw56JGmg=
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b/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=
@ -304,11 +304,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
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/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
@ -344,12 +340,9 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
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.4.2/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/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20161013035702-8b4af36cd21a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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=
@ -379,18 +372,15 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
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=
@ -399,7 +389,6 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
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=
@ -409,9 +398,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
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=
@ -445,44 +434,32 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w
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-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-20210630005230-0f9fa26af87c/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/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
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/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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=
@ -529,15 +506,10 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
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.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
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=
@ -561,7 +533,6 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr
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=
@ -601,9 +572,8 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D
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 h1:PYBmACG+YEv8uQPW0r1kJj8tR+gkF0UWq7iFdUezwEw=
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=
@ -620,6 +590,7 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
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 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
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=
@ -630,17 +601,22 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
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 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
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 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/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=
@ -655,6 +631,8 @@ 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/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
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=
@ -667,8 +645,10 @@ 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/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
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=

View File

@ -9,4 +9,4 @@ tags: ["test"]
metadata:
paths:
icon: /icon.png
minimumRole: visitor
minimumRole: superadmin

View File

@ -8,7 +8,10 @@
<link rel="stylesheet" href="/vendor/mocha.css" />
<style>
body {
background-color: white;
background-color: #f7f7f7;
}
body:not([edge-auto-padding="false"]) #mocha-stats {
top: 75px !important;
}
</style>
</head>
@ -30,6 +33,17 @@
<script src="/test/fetch-module.js"></script>
<script class="mocha-exec">
mocha.run();
Edge.Menu
.setAppIconUrl('/icon.png')
.setAppTitle('SDK Tests')
.setItem('client', 'Client', { linkUrl: '/?grep=Edge', order: 1 })
.setItem('auth-module', 'Auth Module', { linkUrl: '/?grep=Auth%20Module' , order: 4})
.setItem('net-module', 'Net Module', { linkUrl: '/?grep=Net%20Module' , order: 3})
.setItem('rpc', 'Remote Procedure Call', { linkUrl: '/?grep=Remote%20Procedure%20Call' , order: 5})
.setItem('file-module', 'File Module', { linkUrl: '/?grep=File%20Module', order: 6})
.setItem('app-module', 'App Module', { linkUrl: '/?grep=App%20Module' , order: 7})
.setItem('fetch-module', 'Fetch Module', { linkUrl: '/?grep=Fetch%20Module' , order: 8})
</script>
</body>
</html>

View File

@ -1,15 +1,15 @@
describe('App Module', function() {
before(() => {
return Edge.connect();
return Edge.Client.connect();
});
after(() => {
Edge.disconnect();
Edge.Client.disconnect();
});
it('should list apps', function() {
return Edge.rpc("listApps")
return Edge.Client.rpc("listApps")
.then(apps => {
console.log("listApps result:", apps);
chai.assert.isNotNull(apps);
@ -18,7 +18,7 @@ describe('App Module', function() {
});
it('should retrieve requested app', function() {
return Edge.rpc("getApp", { appId: "edge.sdk.client.test" })
return Edge.Client.rpc("getApp", { appId: "edge.sdk.client.test" })
.then(app => {
console.log("getApp result:", app);
chai.assert.isNotNull(app);
@ -27,7 +27,7 @@ describe('App Module', function() {
});
it('should retrieve requested app url without from address', function() {
return Edge.rpc("getAppUrl", { appId: "edge.sdk.client.test" })
return Edge.Client.rpc("getAppUrl", { appId: "edge.sdk.client.test" })
.then(url => {
console.log("getAppUrl result:", url);
chai.assert.isNotEmpty(url);
@ -35,7 +35,7 @@ describe('App Module', function() {
});
it('should retrieve requested app url with from address', function() {
return Edge.rpc("getAppUrl", { appId: "edge.sdk.client.test", from: "127.0.0.2" })
return Edge.Client.rpc("getAppUrl", { appId: "edge.sdk.client.test", from: "127.0.0.2" })
.then(url => {
console.log("getAppUrl result:", url);
chai.assert.isNotEmpty(url);

View File

@ -1,15 +1,15 @@
describe('Auth Module', function() {
before(() => {
return Edge.connect();
return Edge.Client.connect();
});
after(() => {
Edge.disconnect();
Edge.Client.disconnect();
});
it('should retrieve user informations', function() {
return Edge.rpc("getUserInfo")
return Edge.Client.rpc("getUserInfo")
.then(userInfo => {
console.log("getUserInfo result:", userInfo);
chai.assert.property(userInfo, 'subject');

View File

@ -1,25 +1,25 @@
Edge.debug = true;
EdgeFrame.debug = true;
Edge.Client.debug = true;
Edge.Frame.debug = true;
describe('Edge', function() {
describe('#connect()', function() {
after(() => {
Edge.disconnect();
Edge.Client.disconnect();
});
it('should open the connection', function() {
return Edge.connect()
return Edge.Client.connect()
.then(() => {
chai.assert.isNotNull(Edge._conn);
chai.assert.isNotNull(Edge.Client._conn);
});
});
});
describe('#disconnect()', function() {
it('should close the connection', function() {
Edge.disconnect();
chai.assert.isNull(Edge._conn);
Edge.Client.disconnect();
chai.assert.isNull(Edge.Client._conn);
});
});

View File

@ -1,15 +1,15 @@
describe('Fetch Module', function () {
before(() => {
return Edge.connect();
return Edge.Client.connect();
});
after(() => {
Edge.disconnect();
Edge.Client.disconnect();
});
it('should fetch an authorized external url', function () {
var externalUrl = Edge.externalUrl("http://example.com");
var externalUrl = Edge.Client.externalUrl("http://example.com");
return fetch(externalUrl)
.then(res => {
@ -22,7 +22,7 @@ describe('Fetch Module', function () {
});
it('should not fetch an unauthorized external url', function () {
var externalUrl = Edge.externalUrl("https://google.com");
var externalUrl = Edge.Client.externalUrl("https://google.com");
return fetch(externalUrl)
.then(res => {

View File

@ -1,25 +1,25 @@
describe('File Module', function () {
before(() => {
return Edge.connect();
return Edge.Client.connect();
});
after(() => {
Edge.disconnect();
Edge.Client.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)
return Edge.Client.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);
const blobUrl = Edge.Client.blobUrl(result.bucket, result.blobId);
chai.assert.isNotEmpty(blobUrl);
return fetch(blobUrl)

View File

@ -2,11 +2,11 @@ describe('Net Module', function () {
this.timeout(5000);
before(() => {
return Edge.connect();
return Edge.Client.connect();
});
after(() => {
Edge.disconnect();
Edge.Client.disconnect();
});
it('should broadcast a message from server', function (done) {
@ -18,12 +18,12 @@ describe('Net Module', function () {
chai.assert.deepEqual(message, evt.detail);
Edge.removeEventListener('message', handler);
Edge.Client.removeEventListener('message', handler);
done();
};
Edge.addEventListener("message", handler);
Edge.send(message);
Edge.Client.addEventListener("message", handler);
Edge.Client.send(message);
});
it('should send a message to the server and echo back', function(done) {
@ -35,15 +35,15 @@ describe('Net Module', function () {
chai.assert.equal(receivedMessage.now, now.toJSON());
Edge.removeEventListener('message', handler);
Edge.Client.removeEventListener('message', handler);
done();
}
// Server should echo back message
Edge.addEventListener('message', handler);
Edge.Client.addEventListener('message', handler);
// Send message to server
Edge.send({ test: 'echo', now });
Edge.Client.send({ test: 'echo', now });
});
});

View File

@ -1,17 +1,17 @@
describe('Remote Procedure Call', function () {
before(() => {
return Edge.connect();
return Edge.Client.connect();
});
after(() => {
Edge.disconnect();
Edge.Client.disconnect();
});
it('should call the remote echo() method and resolve the returned value', function () {
const foo = "bar";
return Edge.rpc('echo', { foo })
return Edge.Client.rpc('echo', { foo })
.then(result => {
console.log(result);
chai.assert.equal(result.foo, foo);
@ -19,7 +19,7 @@ describe('Remote Procedure Call', function () {
});
it('should call the remote throwErrorFromClient() method and reject with an error', function () {
return Edge.rpc('throwErrorFromClient')
return Edge.Client.rpc('throwErrorFromClient')
.catch(err => {
// Assert that it's an "internal" error
// See https://www.jsonrpc.org/specification#error_object
@ -28,7 +28,7 @@ describe('Remote Procedure Call', function () {
});
it('should call an unregistered method and reject with an error', function () {
return Edge.rpc('unregisteredMethod')
return Edge.Client.rpc('unregisteredMethod')
.catch(err => {
// Assert that it's an "method not found" error
// See https://www.jsonrpc.org/specification#error_object
@ -44,11 +44,11 @@ describe('Remote Procedure Call', function () {
for (let i = 0; i <= 1000; i++) {
values.push((Math.random() * 1000 | 0));
}
return Edge.rpc('reset')
return Edge.Client.rpc('reset')
.then(() => {
return Promise.all(values.map(v => Edge.rpc("add", { value: v })));
return Promise.all(values.map(v => Edge.Client.rpc("add", { value: v })));
})
.then(() => Edge.rpc('total'))
.then(() => Edge.Client.rpc('total'))
.then(remoteTotal => {
const localTotal = values.reduce((t, v) => t + v);
console.log("Remote total:", remoteTotal, "Local total:", localTotal);

View File

@ -4,7 +4,7 @@ ARG HTTP_PROXY=
ARG HTTPS_PROXY=
ARG http_proxy=
ARG https_proxy=
ARG GO_VERSION=1.19.2
ARG GO_VERSION=1.20.2
# Install dev environment dependencies
RUN export DEBIAN_FRONTEND=noninteractive &&\

View File

@ -6,8 +6,11 @@ misc/client-sdk-testsuite/src/**/*
modd.conf
{
prep: make build-sdk
prep: cd misc/client-sdk-testsuite && make dist
prep: make build-client-sdk-test-app
prep: make build
prep: make GOTEST_ARGS="-short" test
daemon: bin/cli app run -p misc/client-sdk-testsuite/dist
daemon: make run-app
}
**/*.go {
prep: make GOTEST_ARGS="-short" test
}

120
package-lock.json generated
View File

@ -10,14 +10,50 @@
"license": "AGPL-3.0",
"dependencies": {
"@types/sockjs-client": "^1.5.1",
"@webcomponents/webcomponentsjs": "^2.8.0",
"core-js": "^3.30.1",
"lit": "^2.7.2",
"sockjs-client": "^1.6.1"
}
},
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.0.tgz",
"integrity": "sha512-92uQ5ARf7UXYrzaFcAX3T2rTvaS9Z1//ukV+DqjACM4c8s0ZBQd7ayJU5Dh2AFLD/Ayuyz4uMmxQec8q3U4Ong=="
},
"node_modules/@lit/reactive-element": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.1.tgz",
"integrity": "sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.0.0"
}
},
"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/@types/trusted-types": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
},
"node_modules/@webcomponents/webcomponentsjs": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz",
"integrity": "sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w=="
},
"node_modules/core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
@ -55,6 +91,34 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/lit": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.7.2.tgz",
"integrity": "sha512-9QnZmG5mIKPRja96cpndMclLSi0Qrz2BXD6EbqNqCKMMjOWVm/BwAeXufFk2jqFsNmY07HOzU8X+8aTSVt3yrA==",
"dependencies": {
"@lit/reactive-element": "^1.6.0",
"lit-element": "^3.3.0",
"lit-html": "^2.7.0"
}
},
"node_modules/lit-element": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.1.tgz",
"integrity": "sha512-Gl+2409uXWbf7n6cCl7Kzasm7zjT9xmdwi2BhLNi70sRKAgRkqueDu5mSIH3hPYMM0/vqBCdPXod3NbGkRA2ww==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.1.0",
"@lit/reactive-element": "^1.3.0",
"lit-html": "^2.7.0"
}
},
"node_modules/lit-html": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.2.tgz",
"integrity": "sha512-ZJCfKlA2XELu5tn7XuzOziGFGvf1SeQm+ngLWoJ8bXtSkRrrR3ms6SWy+gsdxeYwySLij5xAhdd2C3EX0ftxdQ==",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -139,11 +203,39 @@
}
},
"dependencies": {
"@lit-labs/ssr-dom-shim": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.0.tgz",
"integrity": "sha512-92uQ5ARf7UXYrzaFcAX3T2rTvaS9Z1//ukV+DqjACM4c8s0ZBQd7ayJU5Dh2AFLD/Ayuyz4uMmxQec8q3U4Ong=="
},
"@lit/reactive-element": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.1.tgz",
"integrity": "sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==",
"requires": {
"@lit-labs/ssr-dom-shim": "^1.0.0"
}
},
"@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=="
},
"@types/trusted-types": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
},
"@webcomponents/webcomponentsjs": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz",
"integrity": "sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w=="
},
"core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ=="
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
@ -175,6 +267,34 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"lit": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.7.2.tgz",
"integrity": "sha512-9QnZmG5mIKPRja96cpndMclLSi0Qrz2BXD6EbqNqCKMMjOWVm/BwAeXufFk2jqFsNmY07HOzU8X+8aTSVt3yrA==",
"requires": {
"@lit/reactive-element": "^1.6.0",
"lit-element": "^3.3.0",
"lit-html": "^2.7.0"
}
},
"lit-element": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.1.tgz",
"integrity": "sha512-Gl+2409uXWbf7n6cCl7Kzasm7zjT9xmdwi2BhLNi70sRKAgRkqueDu5mSIH3hPYMM0/vqBCdPXod3NbGkRA2ww==",
"requires": {
"@lit-labs/ssr-dom-shim": "^1.1.0",
"@lit/reactive-element": "^1.3.0",
"lit-html": "^2.7.0"
}
},
"lit-html": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.2.tgz",
"integrity": "sha512-ZJCfKlA2XELu5tn7XuzOziGFGvf1SeQm+ngLWoJ8bXtSkRrrR3ms6SWy+gsdxeYwySLij5xAhdd2C3EX0ftxdQ==",
"requires": {
"@types/trusted-types": "^2.0.2"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@ -10,6 +10,9 @@
"license": "AGPL-3.0",
"dependencies": {
"@types/sockjs-client": "^1.5.1",
"@webcomponents/webcomponentsjs": "^2.8.0",
"core-js": "^3.30.1",
"lit": "^2.7.2",
"sockjs-client": "^1.6.1"
}
}

View File

@ -103,13 +103,19 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
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.Route("/api", func(r chi.Router) {
r.Post("/v1/upload", handler.handleAppUpload)
r.Get("/v1/download/{bucket}/{blobID}", handler.handleAppDownload)
r.Get("/fetch", handler.handleAppFetch)
r.Get("/v1/fetch", handler.handleAppFetch)
})
for _, fn := range opts.HTTPMounts {
r.Group(func(r chi.Router) {
fn(r)
})
}
r.HandleFunc("/sock/*", handler.handleSockJS)
})

View File

@ -7,6 +7,7 @@ import (
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
"github.com/go-chi/chi/v5"
"github.com/igm/sockjs-go/v3/sockjs"
)
@ -16,6 +17,7 @@ type HandlerOptions struct {
ServerModuleFactories []app.ServerModuleFactory
UploadMaxFileSize int64
HTTPClient *http.Client
HTTPMounts []func(r chi.Router)
}
func defaultHandlerOptions() *HandlerOptions {
@ -32,6 +34,7 @@ func defaultHandlerOptions() *HandlerOptions {
HTTPClient: &http.Client{
Timeout: time.Second * 30,
},
HTTPMounts: make([]func(r chi.Router), 0),
}
}
@ -66,3 +69,9 @@ func WithHTTPClient(client *http.Client) HandlerOptionFunc {
opts.HTTPClient = client
}
}
func WithHTTPMounts(mounts ...func(r chi.Router)) HandlerOptionFunc {
return func(opts *HandlerOptions) {
opts.HTTPMounts = mounts
}
}

View File

@ -44,6 +44,10 @@ func (r *Repository) List(ctx context.Context) ([]*app.Manifest, error) {
}
func NewRepository(getURL GetURLFunc, manifests ...*app.Manifest) *Repository {
if manifests == nil {
manifests = make([]*app.Manifest, 0)
}
return &Repository{getURL, manifests}
}

112
pkg/module/app/mount.go Normal file
View File

@ -0,0 +1,112 @@
package app
import (
"net"
"net/http"
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
type MountFunc func(r chi.Router)
type Handler struct {
repo Repository
}
func (h *Handler) serveApps(w http.ResponseWriter, r *http.Request) {
manifests, err := h.repo.List(r.Context())
if err != nil {
logger.Error(r.Context(), "could not retrieve app manifest", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
api.DataResponse(w, http.StatusOK, struct {
Manifests []*app.Manifest `json:"manifests"`
}{
Manifests: manifests,
})
}
func (h *Handler) serveApp(w http.ResponseWriter, r *http.Request) {
appID := app.ID(chi.URLParam(r, "appID"))
manifest, err := h.repo.Get(r.Context(), appID)
if err != nil {
if errors.Is(err, ErrNotFound) {
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
return
}
logger.Error(r.Context(), "could not retrieve app manifest", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
api.DataResponse(w, http.StatusOK, struct {
Manifest *app.Manifest `json:"manifest"`
}{
Manifest: manifest,
})
}
type serveAppURLRequest struct {
From string `json:"from,omitempty"`
}
func (h *Handler) serveAppURL(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := &serveAppURLRequest{}
if ok := api.Bind(w, r, req); !ok {
return
}
appID := app.ID(chi.URLParam(r, "appID"))
from := req.From
if from == "" {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
logger.Warn(ctx, "could not split remote address", logger.E(errors.WithStack(err)))
} else {
from = host
}
}
url, err := h.repo.GetURL(ctx, appID, from)
if err != nil {
if errors.Is(err, ErrNotFound) {
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
return
}
logger.Error(r.Context(), "could not retrieve app url", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
api.DataResponse(w, http.StatusOK, struct {
URL string `json:"url"`
}{
URL: url,
})
}
func Mount(repository Repository) MountFunc {
handler := &Handler{repository}
return func(r chi.Router) {
r.Get("/api/v1/apps", handler.serveApps)
r.Get("/api/v1/apps/{appID}", handler.serveApp)
r.Post("/api/v1/apps/{appID}/url", handler.serveAppURL)
}
}

View File

@ -9,5 +9,5 @@ import (
type Repository interface {
List(context.Context) ([]*app.Manifest, error)
Get(context.Context, app.ID) (*app.Manifest, error)
GetURL(context.Context, app.ID, string) (string, error)
GetURL(ctx context.Context, id app.ID, from string) (string, error)
}

View File

@ -2,7 +2,4 @@ package auth
import "errors"
var (
ErrUnauthenticated = errors.New("unauthenticated")
ErrClaimNotFound = errors.New("claim not found")
)
var ErrUnauthenticated = errors.New("unauthenticated")

View File

@ -110,6 +110,8 @@ func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
return
}
account.Claims[auth.ClaimIssuer] = "local"
token, err := generateSignedToken(h.algo, h.key, account.Claims)
if err != nil {
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))

View File

@ -91,7 +91,7 @@
<form method="post" action="{{ .URL }}">
<div class="form-control">
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{ .Username }}" required />
<input type="text" id="username" name="username" value="{{ .Username }}" required autofocus />
</div>
<div class="form-control">
<label for="password">Password</label>

View File

@ -19,10 +19,10 @@ type GetKeySetFunc func() (jwk.Set, error)
func WithJWT(getKeySet GetKeySetFunc) OptionFunc {
return func(o *Option) {
o.GetClaim = func(ctx context.Context, r *http.Request, claimName string) (string, error) {
claim, err := getClaim[string](r, claimName, getKeySet)
o.GetClaims = func(ctx context.Context, r *http.Request, names ...string) ([]string, error) {
claim, err := getClaims[string](r, getKeySet, names...)
if err != nil {
return "", errors.WithStack(err)
return nil, errors.WithStack(err)
}
return claim, nil
@ -76,28 +76,34 @@ func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
return token, nil
}
func getClaim[T any](r *http.Request, claimAttr string, getKeySet GetKeySetFunc) (T, error) {
func getClaims[T any](r *http.Request, getKeySet GetKeySetFunc, names ...string) ([]T, error) {
token, err := FindToken(r, getKeySet)
if err != nil {
return *new(T), errors.WithStack(err)
return nil, errors.WithStack(err)
}
ctx := r.Context()
mapClaims, err := token.AsMap(ctx)
if err != nil {
return *new(T), errors.WithStack(err)
return nil, errors.WithStack(err)
}
rawClaim, exists := mapClaims[claimAttr]
claims := make([]T, len(names))
for idx, n := range names {
rawClaim, exists := mapClaims[n]
if !exists {
return *new(T), errors.WithStack(ErrClaimNotFound)
continue
}
claim, ok := rawClaim.(T)
if !ok {
return *new(T), errors.Errorf("unexpected claim '%s' to be of type '%T', got '%T'", claimAttr, new(T), rawClaim)
return nil, errors.Errorf("unexpected claim '%s' to be of type '%T', got '%T'", n, new(T), rawClaim)
}
return claim, nil
claims[idx] = claim
}
return claims, nil
}

View File

@ -8,15 +8,21 @@ import (
"forge.cadoles.com/arcad/edge/pkg/module/util"
"github.com/dop251/goja"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const (
ClaimSubject = "sub"
ClaimIssuer = "iss"
ClaimPreferredUsername = "preferred_username"
ClaimEdgeRole = "edge_role"
ClaimEdgeTenant = "edge_tenant"
ClaimEdgeEntrypoint = "edge_entrypoint"
)
type Module struct {
server *app.Server
getClaimFunc GetClaimFunc
getClaims GetClaimsFunc
}
func (m *Module) Name() string {
@ -31,6 +37,22 @@ func (m *Module) Export(export *goja.Object) {
if err := export.Set("CLAIM_SUBJECT", ClaimSubject); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_SUBJECT' property"))
}
if err := export.Set("CLAIM_TENANT", ClaimEdgeTenant); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_TENANT' property"))
}
if err := export.Set("CLAIM_ENTRYPOINT", ClaimEdgeEntrypoint); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_ENTRYPOINT' property"))
}
if err := export.Set("CLAIM_ROLE", ClaimEdgeRole); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_ROLE' property"))
}
if err := export.Set("CLAIM_PREFERRED_USERNAME", ClaimPreferredUsername); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_PREFERRED_USERNAME' property"))
}
}
func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
@ -42,16 +64,21 @@ func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
panic(rt.ToValue(errors.New("could not find http request in context")))
}
claim, err := m.getClaimFunc(ctx, req, claimName)
claim, err := m.getClaims(ctx, req, claimName)
if err != nil {
if errors.Is(err, ErrUnauthenticated) || errors.Is(err, ErrClaimNotFound) {
if errors.Is(err, ErrUnauthenticated) {
return nil
}
panic(rt.ToValue(errors.WithStack(err)))
logger.Error(ctx, "could not retrieve claim", logger.E(errors.WithStack(err)))
return nil
}
return rt.ToValue(claim)
if len(claim) == 0 || claim[0] == "" {
return nil
}
return rt.ToValue(claim[0])
}
func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
@ -63,7 +90,7 @@ func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
return &Module{
server: server,
getClaimFunc: opt.GetClaim,
getClaims: opt.GetClaims,
}
}
}

72
pkg/module/auth/mount.go Normal file
View File

@ -0,0 +1,72 @@
package auth
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
type MountFunc func(r chi.Router)
type Handler struct {
getClaims GetClaimsFunc
profileClaims []string
}
func (h *Handler) serveProfile(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := h.getClaims(ctx, r, h.profileClaims...)
if err != nil {
if errors.Is(err, ErrUnauthenticated) {
api.ErrorResponse(
w, http.StatusUnauthorized,
api.ErrCodeUnauthorized,
nil,
)
return
}
logger.Error(ctx, "could not retrieve claims", logger.E(errors.WithStack(err)))
api.ErrorResponse(
w, http.StatusInternalServerError,
api.ErrCodeUnknownError,
nil,
)
return
}
profile := make(map[string]any)
for idx, cl := range h.profileClaims {
profile[cl] = claims[idx]
}
api.DataResponse(w, http.StatusOK, struct {
Profile map[string]any `json:"profile"`
}{
Profile: profile,
})
}
func Mount(authHandler http.Handler, funcs ...OptionFunc) MountFunc {
opt := defaultOptions()
for _, fn := range funcs {
fn(opt)
}
handler := &Handler{
profileClaims: opt.ProfileClaims,
getClaims: opt.GetClaims,
}
return func(r chi.Router) {
r.Get("/api/v1/profile", handler.serveProfile)
r.Handle("/auth/*", authHandler)
}
}

View File

@ -7,26 +7,41 @@ import (
"github.com/pkg/errors"
)
type GetClaimFunc func(ctx context.Context, r *http.Request, claimName string) (string, error)
type GetClaimsFunc func(ctx context.Context, r *http.Request, claims ...string) ([]string, error)
type Option struct {
GetClaim GetClaimFunc
GetClaims GetClaimsFunc
ProfileClaims []string
}
type OptionFunc func(*Option)
func defaultOptions() *Option {
return &Option{
GetClaim: dummyGetClaim,
GetClaims: dummyGetClaims,
ProfileClaims: []string{
ClaimSubject,
ClaimIssuer,
ClaimEdgeEntrypoint,
ClaimEdgeRole,
ClaimPreferredUsername,
ClaimEdgeTenant,
},
}
}
func dummyGetClaim(ctx context.Context, r *http.Request, claimName string) (string, error) {
return "", errors.Errorf("dummy getclaim func cannot retrieve claim '%s'", claimName)
func dummyGetClaims(ctx context.Context, r *http.Request, claims ...string) ([]string, error) {
return nil, errors.Errorf("dummy getclaim func cannot retrieve claims '%s'", claims)
}
func WithGetClaim(fn GetClaimFunc) OptionFunc {
func WithGetClaims(fn GetClaimsFunc) OptionFunc {
return func(o *Option) {
o.GetClaim = fn
o.GetClaims = fn
}
}
func WithProfileClaims(claims ...string) OptionFunc {
return func(o *Option) {
o.ProfileClaims = claims
}
}

View File

@ -3,10 +3,10 @@ package cast
import (
"context"
"net"
"sync"
"time"
"github.com/barnybug/go-cast"
"github.com/barnybug/go-cast/discovery"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
@ -18,6 +18,15 @@ type Device struct {
Name string `goja:"name" json:"name"`
}
type CachedDevice struct {
Device
UpdatedAt time.Time
}
func (d CachedDevice) Expired() bool {
return d.UpdatedAt.Add(30 * time.Minute).Before(time.Now())
}
type DeviceStatus struct {
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
Volume DeviceStatusVolume `goja:"volume" json:"volume"`
@ -35,9 +44,36 @@ type DeviceStatusVolume struct {
}
const (
serviceDiscoveryPollingInterval time.Duration = 2 * time.Second
serviceDiscoveryPollingInterval time.Duration = 500 * time.Millisecond
)
var cache sync.Map
func getCachedDevice(uuid string) (Device, bool) {
value, exists := cache.Load(uuid)
if !exists {
return Device{}, false
}
cachedDevice, ok := value.(CachedDevice)
if !ok {
return Device{}, false
}
if cachedDevice.Expired() {
return Device{}, false
}
return cachedDevice.Device, true
}
func cacheDevice(dev Device) {
cache.Store(dev.UUID, CachedDevice{
Device: dev,
UpdatedAt: time.Now(),
})
}
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
device, err := FindDeviceByUUID(ctx, uuid)
if err != nil {
@ -49,82 +85,114 @@ func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, erro
return client, nil
}
func FindDeviceByUUID(ctx context.Context, uuid string) (*Device, error) {
service := discovery.NewService(ctx)
defer service.Stop()
go func() {
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil {
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err)))
func FindDeviceByUUID(ctx context.Context, uuid string) (Device, error) {
device, exists := getCachedDevice(uuid)
if exists {
return device, nil
}
}()
LOOP:
for {
select {
case c := <-service.Found():
if c.Uuid() == uuid {
return &Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
}, nil
ctx, cancel := context.WithCancel(ctx)
defer cancel()
devices, err := SearchDevices(ctx)
if err != nil {
return Device{}, nil
}
case <-ctx.Done():
break LOOP
for dev := range devices {
if dev.UUID == uuid {
return dev, nil
}
}
if err := ctx.Err(); err != nil {
return nil, errors.WithStack(err)
}
return nil, errors.WithStack(ErrDeviceNotFound)
return Device{}, errors.Errorf("could not find device '%s'", uuid)
}
func FindDevices(ctx context.Context) ([]*Device, error) {
service := discovery.NewService(ctx)
defer service.Stop()
func ListDevices(ctx context.Context, refresh bool) ([]Device, error) {
devices := make([]Device, 0)
go func() {
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err)))
}
}()
devices := make([]*Device, 0)
found := make(map[string]struct{})
LOOP:
for {
select {
case c := <-service.Found():
if _, exists := found[c.Uuid()]; exists {
continue
if !refresh {
cache.Range(func(key, value any) bool {
cached, ok := value.(CachedDevice)
if !ok || cached.Expired() {
return true
}
devices = append(devices, &Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
devices = append(devices, cached.Device)
return true
})
found[c.Uuid()] = struct{}{}
case <-ctx.Done():
break LOOP
}
return devices, nil
}
if err := ctx.Err(); err != nil && !errors.Is(err, context.DeadlineExceeded) {
ch, err := SearchDevices(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
for dev := range ch {
devices = append(devices, dev)
}
return devices, nil
}
var searchDevicesMutex sync.Mutex
func SearchDevices(ctx context.Context) (chan Device, error) {
service := NewService(ctx)
defer service.Stop()
go func() {
searchDevicesMutex.Lock()
defer searchDevicesMutex.Unlock()
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err)))
}
}()
devices := make(chan Device)
go func() {
defer close(devices)
found := make(map[string]struct{})
LOOP:
for {
select {
case c := <-service.Found():
dev := Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
}
if _, exists := found[dev.UUID]; !exists {
devices <- dev
found[dev.UUID] = struct{}{}
}
cacheDevice(dev)
case <-ctx.Done():
break LOOP
}
}
}()
return devices, nil
}
var loadURLMutex sync.Mutex
func LoadURL(ctx context.Context, deviceUUID string, url string) error {
loadURLMutex.Lock()
defer loadURLMutex.Unlock()
client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil {
return errors.WithStack(err)
@ -153,7 +221,12 @@ func isLoadURLContextExceeded(err error) bool {
return err.Error() == "Failed to send load command: context deadline exceeded"
}
var stopCastMutex sync.Mutex
func StopCast(ctx context.Context, deviceUUID string) error {
stopCastMutex.Lock()
defer stopCastMutex.Unlock()
client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil {
return errors.WithStack(err)
@ -171,7 +244,12 @@ func StopCast(ctx context.Context, deviceUUID string) error {
return nil
}
var getStatusMutex sync.Mutex
func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) {
getStatusMutex.Lock()
defer getStatusMutex.Unlock()
client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil {
return nil, errors.WithStack(err)

View File

@ -26,11 +26,24 @@ func TestCastLoadURL(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
devices, err := FindDevices(ctx)
devices, err := ListDevices(ctx, true)
if err != nil {
t.Error(errors.WithStack(err))
}
t.Logf("DEVICES: %s", spew.Sdump(devices))
if e, g := 1, len(devices); e != g {
t.Fatalf("len(devices): expected '%v', got '%v'", e, g)
}
devices, err = ListDevices(ctx, false)
if err != nil {
t.Error(errors.WithStack(err))
}
t.Logf("CACHED DEVICES: %s", spew.Sdump(devices))
if e, g := 1, len(devices); e != g {
t.Fatalf("len(devices): expected '%v', got '%v'", e, g)
}
@ -52,7 +65,7 @@ func TestCastLoadURL(t *testing.T) {
t.Error(errors.WithStack(err))
}
spew.Dump(status)
t.Logf("DEVICE STATUS: %s", spew.Sdump(status))
ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel4()

View File

@ -0,0 +1,250 @@
package cast
import (
"context"
"net"
"regexp"
"strconv"
"strings"
"sync"
"time"
"gitlab.com/wpetit/goweb/logger"
"github.com/barnybug/go-cast"
"github.com/barnybug/go-cast/log"
"github.com/hashicorp/mdns"
"github.com/pkg/errors"
)
type Service struct {
found chan *cast.Client
entriesCh chan *mdns.ServiceEntry
stopPeriodic chan struct{}
}
func NewService(ctx context.Context) *Service {
s := &Service{
found: make(chan *cast.Client),
entriesCh: make(chan *mdns.ServiceEntry, 10),
}
go s.listener(ctx)
return s
}
func (d *Service) Run(ctx context.Context, interval time.Duration) error {
ifaces, err := findMulticastInterfaces(ctx)
if err != nil {
return errors.WithStack(err)
}
var wg sync.WaitGroup
for _, iface := range ifaces {
hasIPv4, hasIPv6, err := retrieveSupportedProtocols(iface)
if err != nil {
return errors.WithStack(err)
}
if !hasIPv4 && !hasIPv6 {
continue
}
if err := d.queryIface(iface, !hasIPv4, !hasIPv6); err != nil {
return errors.WithStack(err)
}
pollCtx, cancel := context.WithCancel(ctx)
defer cancel()
wg.Add(1)
go func(ctx context.Context, iface net.Interface) {
defer wg.Done()
if err := d.pollInterface(ctx, iface, interval, !hasIPv4, !hasIPv6); err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return
}
logger.Error(
ctx, "could not poll interface",
logger.E(errors.WithStack(err)), logger.F("iface", iface.Name),
)
}
}(pollCtx, iface)
}
wg.Wait()
return nil
}
func (d *Service) queryIface(iface net.Interface, disableIPv4, disableIPv6 bool) error {
err := mdns.Query(&mdns.QueryParam{
Service: "_googlecast._tcp",
Domain: "local",
Timeout: 3 * time.Second,
Entries: d.entriesCh,
Interface: &iface,
DisableIPv6: disableIPv6,
DisableIPv4: disableIPv4,
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (d *Service) pollInterface(ctx context.Context, iface net.Interface, interval time.Duration, disableIPv4, disableIPv6 bool) error {
ticker := time.NewTicker(interval)
for {
select {
case <-ticker.C:
if err := d.queryIface(iface, disableIPv4, disableIPv6); err != nil {
return errors.WithStack(err)
}
case <-ctx.Done():
if err := ctx.Err(); err != nil {
return errors.WithStack(err)
}
return nil
}
}
}
func (d *Service) Stop() {
if d.stopPeriodic != nil {
close(d.stopPeriodic)
d.stopPeriodic = nil
}
}
func (d *Service) Found() chan *cast.Client {
return d.found
}
func (d *Service) listener(ctx context.Context) {
for entry := range d.entriesCh {
name := strings.Split(entry.Name, "._googlecast")
// Skip everything that doesn't have googlecast in the fdqn
if len(name) < 2 {
continue
}
log.Printf("New entry: %#v\n", entry)
client := cast.NewClient(entry.AddrV4, entry.Port)
info := decodeTxtRecord(entry.Info)
client.SetName(info["fn"])
client.SetInfo(info)
select {
case d.found <- client:
case <-time.After(time.Second):
case <-ctx.Done():
break
}
}
}
func decodeDnsEntry(text string) string {
text = strings.Replace(text, `\.`, ".", -1)
text = strings.Replace(text, `\ `, " ", -1)
re := regexp.MustCompile(`([\\][0-9][0-9][0-9])`)
text = re.ReplaceAllStringFunc(text, func(source string) string {
i, err := strconv.Atoi(source[1:])
if err != nil {
return ""
}
return string([]byte{byte(i)})
})
return text
}
func decodeTxtRecord(txt string) map[string]string {
m := make(map[string]string)
s := strings.Split(txt, "|")
for _, v := range s {
s := strings.Split(v, "=")
if len(s) == 2 {
m[s[0]] = s[1]
}
}
return m
}
func isIPv4(ip net.IP) bool {
return strings.Count(ip.String(), ":") < 2
}
func isIPv6(ip net.IP) bool {
return strings.Count(ip.String(), ":") >= 2
}
func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, nil
}
multicastIfaces := make([]net.Interface, 0)
for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback == net.FlagLoopback {
continue
}
if iface.Flags&net.FlagRunning != net.FlagRunning {
continue
}
if iface.Flags&net.FlagMulticast != net.FlagMulticast {
continue
}
multicastIfaces = append(multicastIfaces, iface)
}
return multicastIfaces, nil
}
func retrieveSupportedProtocols(iface net.Interface) (bool, bool, error) {
adresses, err := iface.Addrs()
if err != nil {
return false, false, errors.WithStack(err)
}
hasIPv4 := false
hasIPv6 := false
for _, addr := range adresses {
ip, _, err := net.ParseCIDR(addr.String())
if err != nil {
return false, false, errors.WithStack(err)
}
if isIPv4(ip) {
hasIPv4 = true
}
if isIPv6(ip) {
hasIPv6 = true
}
if hasIPv4 && hasIPv6 {
return hasIPv4, hasIPv6, nil
}
}
return hasIPv4, hasIPv6, nil
}

View File

@ -2,7 +2,6 @@ package cast
import (
"context"
"sync"
"time"
"forge.cadoles.com/arcad/edge/pkg/app"
@ -19,14 +18,6 @@ const (
type Module struct {
ctx context.Context
server *app.Server
mutex struct {
devices sync.RWMutex
refreshDevices sync.Mutex
loadURL sync.Mutex
quitApp sync.Mutex
getStatus sync.Mutex
}
devices []*Device
}
func (m *Module) Name() string {
@ -66,14 +57,11 @@ func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.V
promise := m.server.NewPromise()
go func() {
m.mutex.refreshDevices.Lock()
defer m.mutex.refreshDevices.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
devices, err := FindDevices(ctx)
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
devices, err := ListDevices(ctx, true)
if err != nil {
err = errors.WithStack(err)
logger.Error(ctx, "error refreshing casting devices list", logger.E(errors.WithStack(err)))
@ -82,24 +70,19 @@ func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.V
return
}
if err == nil {
m.mutex.devices.Lock()
m.devices = devices
m.mutex.devices.Unlock()
}
devicesCopy := m.getDevicesCopy(devices)
promise.Resolve(devicesCopy)
promise.Resolve(devices)
}()
return rt.ToValue(promise)
}
func (m *Module) getDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
m.mutex.devices.RLock()
defer m.mutex.devices.RUnlock()
ctx := context.Background()
devices := m.getDevicesCopy(m.devices)
devices, err := ListDevices(ctx, false)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(devices)
}
@ -122,9 +105,6 @@ func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
promise := m.server.NewPromise()
go func() {
m.mutex.loadURL.Lock()
defer m.mutex.loadURL.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
@ -160,9 +140,6 @@ func (m *Module) stopCast(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
promise := m.server.NewPromise()
go func() {
m.mutex.quitApp.Lock()
defer m.mutex.quitApp.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
@ -198,9 +175,6 @@ func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value
promise := m.server.NewPromise()
go func() {
m.mutex.getStatus.Lock()
defer m.mutex.getStatus.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
@ -220,21 +194,6 @@ func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value
return m.server.ToValue(promise)
}
func (m *Module) getDevicesCopy(devices []*Device) []Device {
devicesCopy := make([]Device, 0, len(m.devices))
for _, d := range devices {
devicesCopy = append(devicesCopy, Device{
UUID: d.UUID,
Name: d.Name,
Host: d.Host,
Port: d.Port,
})
}
return devicesCopy
}
func (m *Module) parseTimeout(rawTimeout string) (time.Duration, error) {
var (
timeout time.Duration
@ -257,7 +216,6 @@ func CastModuleFactory() app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
return &Module{
server: server,
devices: make([]*Device, 0),
}
}
}

View File

@ -0,0 +1,8 @@
package share
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrAttributeRequired = errors.New("attribute required")
)

341
pkg/module/share/module.go Normal file
View File

@ -0,0 +1,341 @@
package share
import (
"time"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module/util"
"github.com/dop251/goja"
"github.com/pkg/errors"
)
const (
AnyType ValueType = "*"
AnyName string = "*"
)
type Module struct {
appID app.ID
repository Repository
}
func (m *Module) Name() string {
return "share"
}
func (m *Module) Export(export *goja.Object) {
if err := export.Set("upsertResource", m.upsertResource); err != nil {
panic(errors.Wrap(err, "could not set 'upsertResource' function"))
}
if err := export.Set("findResources", m.findResources); err != nil {
panic(errors.Wrap(err, "could not set 'findResources' function"))
}
if err := export.Set("deleteAttributes", m.deleteAttributes); err != nil {
panic(errors.Wrap(err, "could not set 'deleteAttributes' function"))
}
if err := export.Set("deleteResource", m.deleteResource); err != nil {
panic(errors.Wrap(err, "could not set 'deleteResource' function"))
}
if err := export.Set("ANY_TYPE", AnyType); err != nil {
panic(errors.Wrap(err, "could not set 'ANY_TYPE' property"))
}
if err := export.Set("ANY_NAME", AnyName); err != nil {
panic(errors.Wrap(err, "could not set 'ANY_NAME' property"))
}
if err := export.Set("TYPE_TEXT", TypeText); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_TEXT' property"))
}
if err := export.Set("TYPE_NUMBER", TypeNumber); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_NUMBER' property"))
}
if err := export.Set("TYPE_BOOL", TypeBool); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_BOOL' property"))
}
if err := export.Set("TYPE_PATH", TypePath); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_PATH' property"))
}
}
func (m *Module) upsertResource(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt)
var attributes []Attribute
if len(call.Arguments) > 2 {
attributes = assertAttributes(call.Arguments[2:], rt)
} else {
attributes = make([]Attribute, 0)
}
for _, attr := range attributes {
if err := AssertType(attr.Value(), attr.Type()); err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
}
resource, err := m.repository.UpdateAttributes(ctx, m.appID, resourceID, attributes...)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(toGojaResource(resource))
}
func (m *Module) deleteAttributes(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt)
var names []string
if len(call.Arguments) > 2 {
names = assertStrings(call.Arguments[2:], rt)
} else {
names = make([]string, 0)
}
err := m.repository.DeleteAttributes(ctx, m.appID, resourceID, names...)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return nil
}
func (m *Module) findResources(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
funcs := make([]FindResourcesOptionFunc, 0)
if len(call.Arguments) > 1 {
name := util.AssertString(call.Argument(1), rt)
if name != AnyName {
funcs = append(funcs, WithName(name))
}
}
if len(call.Arguments) > 2 {
valueType := assertValueType(call.Argument(2), rt)
if valueType != AnyType {
funcs = append(funcs, WithType(valueType))
}
}
resources, err := m.repository.FindResources(ctx, funcs...)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(toGojaResources(resources))
}
func (m *Module) deleteResource(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt)
err := m.repository.DeleteResource(ctx, m.appID, resourceID)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return nil
}
func ModuleFactory(appID app.ID, repository Repository) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
return &Module{
appID: appID,
repository: repository,
}
}
}
func assertResourceID(v goja.Value, r *goja.Runtime) ResourceID {
value := v.Export()
switch typ := value.(type) {
case string:
return ResourceID(typ)
case ResourceID:
return typ
default:
panic(r.ToValue(errors.Errorf("expected value to be a string or ResourceID, got '%T'", value)))
}
}
func assertAttributes(values []goja.Value, r *goja.Runtime) []Attribute {
attributes := make([]Attribute, len(values))
for idx, val := range values {
export := val.Export()
rawAttr, ok := export.(map[string]any)
if !ok {
panic(r.ToValue(errors.Errorf("unexpected attribute value, got '%v'", export)))
}
rawName, exists := rawAttr["name"]
if !exists {
panic(r.ToValue(errors.Errorf("could not find 'name' property on attribute '%v'", export)))
}
name, ok := rawName.(string)
if !ok {
panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'name': expected 'string', got '%T'", rawName)))
}
rawType, exists := rawAttr["type"]
if !exists {
panic(r.ToValue(errors.Errorf("could not find 'type' property on attribute '%v'", export)))
}
var valueType ValueType
switch typ := rawType.(type) {
case ValueType:
valueType = typ
case string:
valueType = ValueType(typ)
default:
panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'type': expected 'string' or 'ValueType', got '%T'", rawType)))
}
value, exists := rawAttr["value"]
if !exists {
panic(r.ToValue(errors.Errorf("could not find 'value' property on attribute '%v'", export)))
}
attributes[idx] = NewBaseAttribute(
name,
valueType,
value,
)
}
return attributes
}
func assertStrings(values []goja.Value, r *goja.Runtime) []string {
strings := make([]string, len(values))
for idx, v := range values {
strings[idx] = util.AssertString(v, r)
}
return strings
}
func assertValueType(v goja.Value, r *goja.Runtime) ValueType {
value := v.Export()
switch typ := value.(type) {
case string:
return ValueType(typ)
case ValueType:
return typ
default:
panic(r.ToValue(errors.Errorf("expected value to be a string or ValueType, got '%T'", value)))
}
}
type gojaResource struct {
ID ResourceID `goja:"id" json:"id"`
Origin app.ID `goja:"origin" json:"origin"`
Attributes []*gojaAttribute `goja:"attributes" json:"attributes"`
}
func (r *gojaResource) Has(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
name := util.AssertString(call.Argument(0), rt)
valueType := assertValueType(call.Argument(1), rt)
hasAttr := HasAttribute(toResource(r), name, valueType)
return rt.ToValue(hasAttr)
}
func (r *gojaResource) Get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
name := util.AssertString(call.Argument(0), rt)
valueType := assertValueType(call.Argument(1), rt)
var defaultValue any
if len(call.Arguments) > 2 {
defaultValue = call.Argument(2).Export()
}
attr := GetAttribute(toResource(r), name, valueType)
if attr == nil {
return rt.ToValue(defaultValue)
}
return rt.ToValue(attr.Value())
}
type gojaAttribute struct {
Name string `goja:"name" json:"name"`
Type ValueType `goja:"type" json:"type"`
Value any `goja:"value" json:"value"`
CreatedAt time.Time `goja:"createdAt" json:"createdAt"`
UpdatedAt time.Time `goja:"updatedAt" json:"updatedAt"`
}
func toGojaResource(res Resource) *gojaResource {
attributes := make([]*gojaAttribute, len(res.Attributes()))
for idx, attr := range res.Attributes() {
attributes[idx] = &gojaAttribute{
Name: attr.Name(),
Type: attr.Type(),
Value: attr.Value(),
CreatedAt: attr.CreatedAt(),
UpdatedAt: attr.UpdatedAt(),
}
}
return &gojaResource{
ID: res.ID(),
Origin: res.Origin(),
Attributes: attributes,
}
}
func toGojaResources(resources []Resource) []*gojaResource {
gojaResources := make([]*gojaResource, len(resources))
for idx, res := range resources {
gojaResources[idx] = toGojaResource(res)
}
return gojaResources
}
func toResource(res *gojaResource) Resource {
return NewBaseResource(
res.Origin,
res.ID,
toAttributes(res.Attributes)...,
)
}
func toAttributes(gojaAttributes []*gojaAttribute) []Attribute {
attributes := make([]Attribute, len(gojaAttributes))
for idx, gojaAttr := range gojaAttributes {
attr := NewBaseAttribute(
gojaAttr.Name,
gojaAttr.Type,
gojaAttr.Value,
)
attr.SetCreatedAt(gojaAttr.CreatedAt)
attr.SetUpdatedAt(gojaAttr.UpdatedAt)
attributes[idx] = attr
}
return attributes
}

View File

@ -0,0 +1,30 @@
package share
type FindResourcesOptionFunc func(*FindResourcesOptions)
type FindResourcesOptions struct {
Name *string
ValueType *ValueType
}
func FillFindResourcesOptions(funcs ...FindResourcesOptionFunc) *FindResourcesOptions {
opts := &FindResourcesOptions{}
for _, fn := range funcs {
fn(opts)
}
return opts
}
func WithName(name string) FindResourcesOptionFunc {
return func(opts *FindResourcesOptions) {
opts.Name = &name
}
}
func WithType(valueType ValueType) FindResourcesOptionFunc {
return func(opts *FindResourcesOptions) {
opts.ValueType = &valueType
}
}

View File

@ -0,0 +1,32 @@
package share
import (
"context"
"time"
"forge.cadoles.com/arcad/edge/pkg/app"
)
type ResourceID string
type Resource interface {
ID() ResourceID
Origin() app.ID
Attributes() []Attribute
}
type Attribute interface {
Name() string
Value() any
Type() ValueType
UpdatedAt() time.Time
CreatedAt() time.Time
}
type Repository interface {
DeleteResource(ctx context.Context, origin app.ID, resourceID ResourceID) error
FindResources(ctx context.Context, funcs ...FindResourcesOptionFunc) ([]Resource, error)
GetResource(ctx context.Context, origin app.ID, resourceID ResourceID) (Resource, error)
UpdateAttributes(ctx context.Context, origin app.ID, resourceID ResourceID, attributes ...Attribute) (Resource, error)
DeleteAttributes(ctx context.Context, origin app.ID, resourceID ResourceID, names ...string) error
}

View File

@ -0,0 +1,121 @@
package share
import (
"time"
"forge.cadoles.com/arcad/edge/pkg/app"
)
type BaseResource struct {
id ResourceID
origin app.ID
attributes []Attribute
}
// Attributes implements Resource
func (r *BaseResource) Attributes() []Attribute {
return r.attributes
}
// ID implements Resource
func (r *BaseResource) ID() ResourceID {
return r.id
}
// Origin implements Resource
func (r *BaseResource) Origin() app.ID {
return r.origin
}
func (r *BaseResource) SetAttribute(attr Attribute) {
for idx, rAttr := range r.attributes {
if rAttr.Name() != attr.Name() {
continue
}
r.attributes[idx] = attr
return
}
r.attributes = append(r.attributes, attr)
}
func NewBaseResource(origin app.ID, resourceID ResourceID, attributes ...Attribute) *BaseResource {
return &BaseResource{
id: resourceID,
origin: origin,
attributes: attributes,
}
}
var _ Resource = &BaseResource{}
type BaseAttribute struct {
name string
valueType ValueType
value any
createdAt time.Time
updatedAt time.Time
}
// CreatedAt implements Attribute
func (a *BaseAttribute) CreatedAt() time.Time {
return a.createdAt
}
// Name implements Attribute
func (a *BaseAttribute) Name() string {
return a.name
}
// Type implements Attribute
func (a *BaseAttribute) Type() ValueType {
return a.valueType
}
// UpdatedAt implements Attribute
func (a *BaseAttribute) UpdatedAt() time.Time {
return a.updatedAt
}
// Value implements Attribute
func (a *BaseAttribute) Value() any {
return a.value
}
func (a *BaseAttribute) SetCreatedAt(createdAt time.Time) {
a.createdAt = createdAt
}
func (a *BaseAttribute) SetUpdatedAt(updatedAt time.Time) {
a.updatedAt = updatedAt
}
func NewBaseAttribute(name string, valueType ValueType, value any) *BaseAttribute {
return &BaseAttribute{
name: name,
valueType: valueType,
value: value,
}
}
var _ Attribute = &BaseAttribute{}
func HasAttribute(res Resource, name string, valueType ValueType) bool {
return GetAttribute(res, name, valueType) != nil
}
func GetAttribute(res Resource, name string, valueType ValueType) Attribute {
for _, attr := range res.Attributes() {
if attr.Name() != name {
continue
}
if attr.Type() == valueType {
return attr
}
}
return nil
}

View File

@ -0,0 +1,13 @@
package sqlite
import (
"testing"
"forge.cadoles.com/arcad/edge/pkg/module/share/testsuite"
"gitlab.com/wpetit/goweb/logger"
)
func TestModule(t *testing.T) {
logger.SetLevel(logger.LevelDebug)
testsuite.TestModule(t, newTestRepo)
}

View File

@ -0,0 +1,429 @@
package sqlite
import (
"context"
"database/sql"
"fmt"
"time"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module/share"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Repository struct {
getDB sqlite.GetDBFunc
}
// DeleteAttributes implements share.Repository
func (r *Repository) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error {
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `
DELETE FROM resources
WHERE origin = $1 AND resource_id = $2
`
args := []any{origin, resourceID}
criteria := ""
for idx, name := range names {
if idx == 0 {
criteria += " AND ("
}
if idx != 0 {
criteria += " OR "
}
criteria += fmt.Sprintf(" name = $%d", len(args)+1)
args = append(args, name)
if idx == len(names)-1 {
criteria += " )"
}
}
query += criteria
logger.Debug(
ctx, "executing query",
logger.F("query", query),
logger.F("args", args),
)
res, err := tx.ExecContext(ctx, query, args...)
if err != nil {
return errors.WithStack(err)
}
affected, err := res.RowsAffected()
if err != nil {
return errors.WithStack(err)
}
if affected == 0 {
return errors.WithStack(share.ErrNotFound)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return err
}
// DeleteResource implements share.Repository
func (r *Repository) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error {
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `
DELETE FROM resources
WHERE origin = $1 AND resource_id = $2
`
args := []any{origin, resourceID}
logger.Debug(
ctx, "executing query",
logger.F("query", query),
logger.F("args", args),
)
res, err := tx.ExecContext(ctx, query, args...)
if err != nil {
return errors.WithStack(err)
}
affected, err := res.RowsAffected()
if err != nil {
return errors.WithStack(err)
}
if affected == 0 {
return errors.WithStack(share.ErrNotFound)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return err
}
// FindResources implements share.Repository
func (r *Repository) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) {
opts := share.FillFindResourcesOptions(funcs...)
var resources []share.Resource
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `
SELECT
main.origin, main.resource_id,
main.name, main.type, main.value,
main.created_at, main.updated_at
FROM resources AS main
JOIN resources AS sub ON
main.resource_id = sub.resource_id
AND main.origin = sub.origin
`
criteria := " WHERE 1 = 1"
preparedArgIndex := 1
args := make([]any, 0)
if opts.Name != nil {
criteria += fmt.Sprintf(" AND sub.name = $%d", preparedArgIndex)
args = append(args, *opts.Name)
preparedArgIndex++
}
if opts.ValueType != nil {
criteria += fmt.Sprintf(" AND sub.type = $%d", preparedArgIndex)
args = append(args, *opts.ValueType)
preparedArgIndex++
}
query += criteria
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 func() {
if err := rows.Close(); err != nil {
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
}
}()
indexedResources := make(map[string]*share.BaseResource)
for rows.Next() {
var (
origin string
resourceID string
name string
valueType string
value any
updatedAt time.Time
createdAt time.Time
)
if err := rows.Scan(&origin, &resourceID, &name, &valueType, &value, &createdAt, &updatedAt); err != nil {
return errors.WithStack(err)
}
resourceKey := origin + resourceID
resource, exists := indexedResources[resourceKey]
if !exists {
resource = share.NewBaseResource(app.ID(origin), share.ResourceID(resourceID))
indexedResources[resourceKey] = resource
}
attr := share.NewBaseAttribute(
name,
share.ValueType(valueType),
value,
)
attr.SetCreatedAt(createdAt)
attr.SetUpdatedAt(updatedAt)
resource.SetAttribute(attr)
}
if err := rows.Err(); err != nil {
return errors.WithStack(err)
}
resources = make([]share.Resource, 0, len(indexedResources))
for _, res := range indexedResources {
resources = append(resources, res)
}
return nil
})
if err != nil {
return nil, errors.WithStack(err)
}
return resources, nil
}
// GetResource implements share.Repository
func (r *Repository) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) {
var (
resource *share.BaseResource
err error
)
err = r.withTx(ctx, func(tx *sql.Tx) error {
resource, err = r.getResourceWithinTx(ctx, tx, origin, resourceID)
if err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return nil, errors.WithStack(err)
}
return resource, nil
}
// UpdateAttributes implements share.Repository
func (r *Repository) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) {
if len(attributes) == 0 {
return nil, errors.WithStack(share.ErrAttributeRequired)
}
var resource *share.BaseResource
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `
INSERT INTO resources (origin, resource_id, name, type, value, created_at, updated_at)
VALUES($1, $2, $3, $4, $5, $6, $6)
ON CONFLICT (origin, resource_id, name) DO UPDATE SET
type = $4, value = $5, updated_at = $6
`
stmt, err := tx.PrepareContext(ctx, query)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := stmt.Close(); err != nil {
logger.Error(ctx, "could not close statement", logger.E(errors.WithStack(err)))
}
}()
now := time.Now().UTC()
for _, attr := range attributes {
args := []any{
string(origin), string(resourceID),
attr.Name(), string(attr.Type()), attr.Value(),
now, now,
}
logger.Debug(
ctx, "executing query",
logger.F("query", query),
logger.F("args", args),
)
if _, err := stmt.ExecContext(ctx, args...); err != nil {
return errors.WithStack(err)
}
}
resource, err = r.getResourceWithinTx(ctx, tx, origin, resourceID)
if err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return nil, errors.WithStack(err)
}
return resource, nil
}
func (r *Repository) getResourceWithinTx(ctx context.Context, tx *sql.Tx, origin app.ID, resourceID share.ResourceID) (*share.BaseResource, error) {
query := `
SELECT name, type, value, created_at, updated_at
FROM resources
WHERE origin = $1 AND resource_id = $2
`
rows, err := tx.QueryContext(ctx, query, origin, resourceID)
if err != nil {
return nil, errors.WithStack(err)
}
defer func() {
if err := rows.Close(); err != nil {
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
}
}()
attributes := make([]share.Attribute, 0)
for rows.Next() {
var (
name string
valueType string
value any
updatedAt time.Time
createdAt time.Time
)
if err := rows.Scan(&name, &valueType, &value, &createdAt, &updatedAt); err != nil {
return nil, errors.WithStack(err)
}
attr := share.NewBaseAttribute(
name,
share.ValueType(valueType),
value,
)
attr.SetCreatedAt(createdAt)
attr.SetUpdatedAt(updatedAt)
attributes = append(attributes, attr)
}
if err := rows.Err(); err != nil {
return nil, errors.WithStack(err)
}
if len(attributes) == 0 {
return nil, errors.WithStack(share.ErrNotFound)
}
resource := share.NewBaseResource(origin, resourceID, attributes...)
return resource, nil
}
func (r *Repository) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
var db *sql.DB
db, err := r.getDB(ctx)
if err != nil {
return errors.WithStack(err)
}
if err := sqlite.WithTx(ctx, db, fn); err != nil {
return errors.WithStack(err)
}
return nil
}
func ensureTables(ctx context.Context, db *sql.DB) error {
err := sqlite.WithTx(ctx, db, func(tx *sql.Tx) error {
query := `
CREATE TABLE IF NOT EXISTS resources (
resource_id TEXT NOT NULL,
origin TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
value TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE(origin, resource_id, name) ON CONFLICT REPLACE
);
`
if _, err := tx.ExecContext(ctx, query); err != nil {
return errors.WithStack(err)
}
query = `
CREATE INDEX IF NOT EXISTS resource_idx ON resources (origin, resource_id, name);
`
if _, err := tx.ExecContext(ctx, query); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func NewRepository(path string) *Repository {
getDB := sqlite.NewGetDBFunc(path, ensureTables)
return &Repository{
getDB: getDB,
}
}
func NewRepositoryWithDB(db *sql.DB) *Repository {
getDB := sqlite.NewGetDBFuncFromDB(db, ensureTables)
return &Repository{
getDB: getDB,
}
}
var _ share.Repository = &Repository{}

View File

@ -0,0 +1,33 @@
package sqlite
import (
"fmt"
"os"
"strings"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/module/share"
"forge.cadoles.com/arcad/edge/pkg/module/share/testsuite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestRepository(t *testing.T) {
logger.SetLevel(logger.LevelDebug)
testsuite.TestRepository(t, newTestRepo)
}
func newTestRepo(testName string) (share.Repository, error) {
filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
file := fmt.Sprintf("./testdata/%s.sqlite", filename)
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
repo := NewRepository(dsn)
return repo, nil
}

View File

@ -0,0 +1 @@
*.sqlite*

View File

@ -0,0 +1,47 @@
package testsuite
import (
"context"
"io/fs"
"testing"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/module/share"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestModule(t *testing.T, newRepo NewTestRepoFunc) {
logger.SetLevel(logger.LevelDebug)
repo, err := newRepo("module")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
server := app.NewServer(
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
share.ModuleFactory("test.app.edge", repo),
)
data, err := fs.ReadFile(testData, "testdata/share.js")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if err := server.Load("testdata/share.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(context.Background(), "testModule"); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
server.Stop()
}

View File

@ -0,0 +1,16 @@
package testsuite
import (
"testing"
"forge.cadoles.com/arcad/edge/pkg/module/share"
)
type NewTestRepoFunc func(testname string) (share.Repository, error)
func TestRepository(t *testing.T, newRepo NewTestRepoFunc) {
t.Run("Cases", func(t *testing.T) {
t.Parallel()
runRepositoryTests(t, newRepo)
})
}

View File

@ -0,0 +1,344 @@
package testsuite
import (
"context"
"encoding/json"
"reflect"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module/share"
"github.com/pkg/errors"
)
type repositoryTestCase struct {
Name string
Skip bool
Run func(ctx context.Context, t *testing.T, repo share.Repository) error
}
var repositoryTestCases = []repositoryTestCase{
{
Name: "Update resource attributes",
Skip: false,
Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
origin := app.ID("test")
resourceID := share.ResourceID("test")
// Try to create resource without attributes
_, err := repo.UpdateAttributes(ctx, origin, resourceID)
if err == nil {
return errors.New("err should not be nil")
}
if !errors.Is(err, share.ErrAttributeRequired) {
return errors.Errorf("err: expected share.ErrAttributeRequired, got '%+v'", err)
}
attributes := []share.Attribute{
share.NewBaseAttribute("my_text_attr", share.TypeText, "foo"),
share.NewBaseAttribute("my_number_attr", share.TypeNumber, 5),
share.NewBaseAttribute("my_path_attr", share.TypePath, "/my/path"),
share.NewBaseAttribute("my_bool_attr", share.TypeBool, true),
}
resource, err := repo.UpdateAttributes(ctx, origin, resourceID, attributes...)
if err != nil {
return errors.WithStack(err)
}
isNil := reflect.ValueOf(resource).IsNil()
if isNil {
return errors.New("resource should not be nil")
}
if e, g := resourceID, resource.ID(); e != g {
return errors.Errorf("resource.ID(): expected '%v', got '%v'", e, g)
}
if e, g := origin, resource.Origin(); e != g {
return errors.Errorf("resource.Origin(): expected '%v', got '%v'", e, g)
}
if e, g := 4, len(resource.Attributes()); e != g {
return errors.Errorf("len(resource.Attributes()): expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Find resources by attribute name",
Skip: false,
Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/find_resources_by_attribute_name.json", repo); err != nil {
return errors.WithStack(err)
}
resources, err := repo.FindResources(ctx, share.WithName("my_number"))
if err != nil {
return errors.WithStack(err)
}
isNil := reflect.ValueOf(resources).IsNil()
if isNil {
return errors.New("resources should not be nil")
}
if e, g := 2, len(resources); e != g {
return errors.Errorf("len(resources): expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Find resources by attribute type",
Skip: false,
Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/find_resources_by_attribute_type.json", repo); err != nil {
return errors.WithStack(err)
}
resources, err := repo.FindResources(ctx, share.WithType(share.TypePath))
if err != nil {
return errors.WithStack(err)
}
isNil := reflect.ValueOf(resources).IsNil()
if isNil {
return errors.New("resources should not be nil")
}
if e, g := 1, len(resources); e != g {
return errors.Errorf("len(resources): expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Find resources by attribute type and name",
Skip: false,
Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/find_resources_by_attribute_type_and_name.json", repo); err != nil {
return errors.WithStack(err)
}
resources, err := repo.FindResources(ctx, share.WithType(share.TypeText), share.WithName("my_attr"))
if err != nil {
return errors.WithStack(err)
}
isNil := reflect.ValueOf(resources).IsNil()
if isNil {
return errors.New("resources should not be nil")
}
if e, g := 1, len(resources); e != g {
return errors.Errorf("len(resources): expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Get resource",
Skip: false,
Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/get_resource.json", repo); err != nil {
return errors.WithStack(err)
}
origin := app.ID("app1.edge.app")
resourceID := share.ResourceID("res-1")
resource, err := repo.GetResource(ctx, origin, resourceID)
if err != nil {
return errors.WithStack(err)
}
isNil := reflect.ValueOf(resource).IsNil()
if isNil {
return errors.New("resources should not be nil")
}
if e, g := origin, resource.Origin(); e != g {
return errors.Errorf("resource.Origin(): expected '%v', got '%v'", e, g)
}
if e, g := resourceID, resource.ID(); e != g {
return errors.Errorf("resource.ID(): expected '%v', got '%v'", e, g)
}
resource, err = repo.GetResource(ctx, origin, "unexistant-id")
if err == nil {
return errors.New("err should not be nil")
}
if !errors.Is(err, share.ErrNotFound) {
return errors.Errorf("err: expected share.ErrNotFound, got '%+v'", err)
}
return nil
},
},
{
Name: "Delete resource",
Skip: false,
Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/delete_resource.json", repo); err != nil {
return errors.WithStack(err)
}
origin := app.ID("app1.edge.app")
resourceID := share.ResourceID("res-1")
// It should delete an existing resource
if err := repo.DeleteResource(ctx, origin, resourceID); err != nil {
return errors.WithStack(err)
}
_, err := repo.GetResource(ctx, origin, resourceID)
if err == nil {
return errors.New("err should not be nil")
}
// The resource should be deleted
if !errors.Is(err, share.ErrNotFound) {
return errors.Errorf("err: expected share.ErrNotFound, got '%+v'", err)
}
// It should not delete an unexistant resource
err = repo.DeleteResource(ctx, origin, resourceID)
if err == nil {
return errors.New("err should not be nil")
}
if !errors.Is(err, share.ErrNotFound) {
return errors.Errorf("err: expected share.ErrNotFound, got '%+v'", err)
}
otherOrigin := app.ID("app2.edge.app")
// It should not delete a resource with the same id and another origin
resource, err := repo.GetResource(ctx, otherOrigin, resourceID)
if err != nil {
return errors.New("err should not be nil")
}
if e, g := otherOrigin, resource.Origin(); e != g {
return errors.Errorf("resource.Origin(): expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Delete attributes",
Skip: false,
Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/delete_attributes.json", repo); err != nil {
return errors.WithStack(err)
}
origin := app.ID("app1.edge.app")
resourceID := share.ResourceID("res-1")
// It should delete specified attributes
if err := repo.DeleteAttributes(ctx, origin, resourceID, "my_text", "my_bool"); err != nil {
return errors.WithStack(err)
}
resource, err := repo.GetResource(ctx, origin, resourceID)
if err != nil {
return errors.WithStack(err)
}
if e, g := 1, len(resource.Attributes()); e != g {
return errors.Errorf("len(resource.Attributes()): expected '%v', got '%v'", e, g)
}
attr := share.GetAttribute(resource, "my_number", share.TypeNumber)
if attr == nil {
return errors.New("attr shoudl not be nil")
}
return nil
},
},
}
func runRepositoryTests(t *testing.T, newRepo NewTestRepoFunc) {
for _, tc := range repositoryTestCases {
func(tc repositoryTestCase) {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
if tc.Skip {
t.SkipNow()
return
}
ctx := context.Background()
repo, err := newRepo(tc.Name)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if err := tc.Run(ctx, t, repo); err != nil {
t.Errorf("%+v", errors.WithStack(err))
}
})
}(tc)
}
}
type jsonResource struct {
ID string `json:"id"`
Origin string `json:"origin"`
Attributes []jsonAttribute `json:"attributes"`
}
type jsonAttribute struct {
Name string `json:"name"`
Type share.ValueType `json:"type"`
Value any `json:"value"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func loadTestData(ctx context.Context, jsonFile string, repo share.Repository) error {
data, err := testData.ReadFile(jsonFile)
if err != nil {
return errors.WithStack(err)
}
var resources []jsonResource
if err := json.Unmarshal(data, &resources); err != nil {
return errors.WithStack(err)
}
for _, res := range resources {
attributes := make([]share.Attribute, len(res.Attributes))
for idx, attr := range res.Attributes {
attributes[idx] = share.NewBaseAttribute(
attr.Name,
attr.Type,
attr.Value,
)
}
_, err := repo.UpdateAttributes(ctx, app.ID(res.Origin), share.ResourceID(res.ID), attributes...)
if err != nil {
return errors.WithStack(err)
}
}
return nil
}

View File

@ -0,0 +1,6 @@
package testsuite
import "embed"
//go:embed testdata/*
var testData embed.FS

View File

@ -0,0 +1,11 @@
[
{
"id": "res-1",
"origin": "app1.edge.app",
"attributes": [
{ "name":"my_text", "type": "text", "value": "bar" },
{ "name":"my_bool", "type": "bool", "value": true },
{ "name":"my_number", "type": "number", "value": 5 }
]
}
]

View File

@ -0,0 +1,16 @@
[
{
"id": "res-1",
"origin": "app1.edge.app",
"attributes": [
{ "name":"my_text", "type": "text", "value": "bar" }
]
},
{
"id": "res-1",
"origin": "app2.edge.app",
"attributes": [
{ "name":"my_text", "type": "text", "value": "foo" }
]
}
]

View File

@ -0,0 +1,29 @@
[
{
"id": "res-1",
"origin": "app1.edge.app",
"attributes": [
{ "name":"my_text", "type": "text", "value": "bar" },
{ "name":"my_number", "type": "number", "value": 5 },
{ "name":"my_bool", "type": "bool", "value": true },
{ "name":"my_path", "type": "path", "value": "/my/icon.png" }
]
},
{
"id": "res-2",
"origin": "app1.edge.app",
"attributes": [
{ "name":"other_text", "type": "text", "value": "foo" }
]
},
{
"id": "res-1",
"origin": "app2.edge.app",
"attributes": [
{ "name":"my_text", "type": "text", "value": "bar" },
{ "name":"my_number", "type": "number", "value": 5 },
{ "name":"my_bool", "type": "bool", "value": true },
{ "name":"my_path", "type": "path", "value": "/my/icon.png" }
]
}
]

View File

@ -0,0 +1,28 @@
[
{
"id": "res-1",
"origin": "app1.edge.app",
"attributes": [
{ "name":"my_text", "type": "text", "value": "bar" },
{ "name":"my_number", "type": "number", "value": 5 },
{ "name":"my_bool", "type": "bool", "value": true }
]
},
{
"id": "res-2",
"origin": "app1.edge.app",
"attributes": [
{ "name":"other_text", "type": "text", "value": "foo" }
]
},
{
"id": "res-1",
"origin": "app2.edge.app",
"attributes": [
{ "name":"my_text", "type": "text", "value": "bar" },
{ "name":"my_number", "type": "number", "value": 5 },
{ "name":"my_bool", "type": "bool", "value": true },
{ "name":"my_path", "type": "path", "value": "/my/icon.png" }
]
}
]

View File

@ -0,0 +1,23 @@
[
{
"id": "res-1",
"origin": "app1.edge.app",
"attributes": [
{ "name":"my_attr", "type": "text", "value": "bar" }
]
},
{
"id": "res-2",
"origin": "app1.edge.app",
"attributes": [
{ "name":"my_attr", "type": "bool", "value": true }
]
},
{
"id": "res-1",
"origin": "app2.edge.app",
"attributes": [
{ "name":"my_attr", "type": "number", "value": 5 }
]
}
]

View File

@ -0,0 +1,16 @@
[
{
"id": "res-1",
"origin": "app1.edge.app",
"attributes": [
{ "name":"my_text", "type": "text", "value": "bar" }
]
},
{
"id": "res-1",
"origin": "app2.edge.app",
"attributes": [
{ "name":"my_text", "type": "text", "value": "foo" }
]
}
]

View File

@ -0,0 +1,82 @@
function testModule() {
var ctx = context.new();
var resourceId = "my-first-res";
var attributes = [
{ name: "my_text", type: share.TYPE_TEXT, value: "my_text" },
{ name: "my_number", type: share.TYPE_NUMBER, value: 5 },
{ name: "my_path", type: share.TYPE_PATH, value: "/my/path" },
{ name: "my_bool", type: share.TYPE_BOOL, value: true },
]
// Create resource with attributes
var resource = share.upsertResource(
ctx, resourceId,
attributes[0],
attributes[1],
attributes[2],
attributes[3]
);
if (resource.id != resourceId) {
throw new Error("resource.id: expected '"+resourceId+"', got '"+resource.id+"'")
}
if (resource.origin != "test.app.edge") {
throw new Error("resource.origin: expected 'test.app.edge', got '"+resource.origin+"'")
}
if (resource.attributes.length != 4) {
throw new Error("resource.attributes.length: expected '1', got '"+resource.attributes.length+"'")
}
for(var attr, i = 0;( attr = attributes[i] ); i++) {
var exists = resource.has(attr.name, attr.type);
if (!exists) {
throw new Error("resource.has('"+attr.name+"'): expected 'true', got '"+hasAttr+"'")
}
var value = resource.get(attr.name, attr.type);
if (value != attr.value) {
throw new Error("value: expected '"+attr.value+"', got '"+value+"'")
}
}
// Test acces of unexistant attribute
var unexistantAttr = "unexistant_attr"
var exists = resource.has(unexistantAttr, share.TYPE_TEXT);
if (exists) {
throw new Error("attr '"+unexistantAttr+"' should not exist")
}
var expected = "foo"
var value = resource.get(unexistantAttr, share.TYPE_TEXT, expected);
if (value != expected) {
throw new Error("resource.get('"+attr.name+"', share.TYPE_TEXT, '"+expected+"'): expected '"+expected+"', got '"+value+"'")
}
// Search resources
// With any attribute
var results = share.findResources(ctx, share.ANY_NAME, share.ANY_TYPE);
if (results.length != 1) {
throw new Error("results.length: expected '1', got '"+results.length+"'")
}
// With an unexistant attribute
var results = share.findResources(ctx, unexistantAttr, share.ANY_TYPE);
if (results.length != 0) {
throw new Error("results.length: expected '0', got '"+results.length+"'")
}
// With a wrong type
var results = share.findResources(ctx, "my_text", share.TYPE_NUMBER);
if (results.length != 0) {
throw new Error("results.length: expected '0', got '"+results.length+"'")
}
}

View File

@ -0,0 +1,86 @@
package share
import (
"strings"
"github.com/pkg/errors"
)
type ValueType string
const (
TypeText ValueType = "text"
TypePath ValueType = "path"
TypeNumber ValueType = "number"
TypeBool ValueType = "bool"
)
func AssertType(value any, valueType ValueType) error {
switch valueType {
case TypeText:
if err := AssertTypeText(value); err != nil {
return errors.WithStack(err)
}
case TypeNumber:
if err := AssertTypeNumber(value); err != nil {
return errors.WithStack(err)
}
case TypeBool:
if err := AssertTypeBool(value); err != nil {
return errors.WithStack(err)
}
case TypePath:
if err := AssertTypePath(value); err != nil {
return errors.WithStack(err)
}
default:
return errors.Errorf("value type '%s' does not exist", valueType)
}
return nil
}
func AssertTypeText(value any) error {
_, ok := value.(string)
if !ok {
return errors.Errorf("invalid value for type '%s': '%v'", TypeText, value)
}
return nil
}
func AssertTypeNumber(value any) error {
switch value.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return nil
default:
return errors.Errorf("invalid value for type '%s': '%v'", TypeNumber, value)
}
}
func AssertTypeBool(value any) error {
_, ok := value.(bool)
if !ok {
return errors.Errorf("invalid value for type '%s': '%v'", TypeBool, value)
}
return nil
}
func AssertTypePath(value any) error {
path, ok := value.(string)
if !ok {
return errors.Errorf("invalid value for type '%s': '%v'", TypePath, value)
}
if !strings.HasPrefix(path, "/") {
return errors.Errorf("value '%s' should start with a '/'", value)
}
return nil
}

View File

@ -0,0 +1,88 @@
package share
import (
"testing"
"github.com/pkg/errors"
)
type valueTypeTestCase struct {
Name string
Value any
Type ValueType
ShouldFail bool
}
var valueTypeTestCases = []valueTypeTestCase{
{
Name: "Valid text",
Value: "my_text",
Type: TypeText,
},
{
Name: "Invalid text",
Value: 0,
Type: TypeText,
ShouldFail: true,
},
{
Name: "Valid number",
Value: 5.6,
Type: TypeNumber,
},
{
Name: "Invalid number",
Value: "5",
Type: TypeNumber,
ShouldFail: true,
},
{
Name: "Valid bool",
Value: false,
Type: TypeBool,
},
{
Name: "Invalid bool",
Value: "yes",
Type: TypeBool,
ShouldFail: true,
},
{
Name: "Valid path",
Value: "/foo/bar",
Type: TypePath,
},
{
Name: "Invalid path",
Value: true,
Type: TypePath,
ShouldFail: true,
},
{
Name: "Missing slash",
Value: "missing/slash",
Type: TypePath,
ShouldFail: true,
},
}
func TestAssertType(t *testing.T) {
t.Parallel()
for _, tc := range valueTypeTestCases {
func(tc valueTypeTestCase) {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
err := AssertType(tc.Value, tc.Type)
if tc.ShouldFail && err == nil {
t.Errorf("err should not be nil")
}
if !tc.ShouldFail && err != nil {
t.Errorf("err: expected nil, got '%+v'", errors.WithStack(err))
}
})
}(tc)
}
}

View File

@ -3,6 +3,7 @@ package util
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/dop251/goja"
"github.com/pkg/errors"
)
@ -26,3 +27,15 @@ func AssertObject(v goja.Value, r *goja.Runtime) map[string]any {
func AssertString(v goja.Value, r *goja.Runtime) string {
return AssertType[string](v, r)
}
func AssertAppID(v goja.Value, r *goja.Runtime) app.ID {
value := v.Export()
switch typ := value.(type) {
case string:
return app.ID(typ)
case app.ID:
return typ
default:
panic(r.ToValue(errors.Errorf("expected value to be a string or app.ID, got '%T'", value)))
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" />
</svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@ -0,0 +1,9 @@
import UserCircleIcon from './user-circle.svg';
import MenuIcon from './menu.svg';
import CloudIcon from './cloud.svg';
import LoginIcon from './login.svg';
import HomeIcon from './home.svg';
import LinkIcon from './link.svg';
import LogoutIcon from './logout.svg';
export { UserCircleIcon, MenuIcon, CloudIcon, LoginIcon, HomeIcon, LinkIcon, LogoutIcon }

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>

After

Width:  |  Height:  |  Size: 376 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
</svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg>

After

Width:  |  Height:  |  Size: 362 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>

After

Width:  |  Height:  |  Size: 260 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@ -0,0 +1,130 @@
import { LitElement, html, css } from 'lit';
import { property, state } from 'lit/decorators.js';
export const EVENT_MENU_ITEM_SELECTED = 'menu-item-selected';
export const EVENT_MENU_ITEM_UNSELECTED = 'menu-item-unselected';
export class MenuItem extends LitElement {
@property({ attribute: 'icon-url', type: String })
iconUrl: string;
@property({ attribute: 'label', type: String })
label: string;
static styles = css`
:host {
display: inline-block;
height: 100%;
flex: 1;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-bottom: 1px solid rgb(229,231,235);
border-top: 10px solid transparent;
transition: all 100ms ease-out;
background-color: #fff;
}
:host(:hover) {
background-color: rgb(249,250,251);
}
:host(.selected) {
border-top: 10px solid #03A9F4;
border-bottom: 1px solid transparent;
background-color: #fff;
}
:host(.unselected) {
background-color: hsl(210 20% 95% / 1);
}
.menu-item-icon {
height: 30px;
width: 30px;
overflow: hidden;
}
.menu-item-icon > img {
width: 100%;
height: 100%;
}
.menu-item-panel {
display: none;
position: fixed;
top: 65px;
left: 0;
right: 0;
z-index: 9999;
background-color: #fff;
box-shadow: 0px 4px 5px 0px hsl(0deg 0% 0% / 10%);
max-height: 75%;
overflow-y: auto;
}
:host(.selected) .menu-item-panel {
display: block;
}
.menu-item-label {
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
color: black;
font-size: 14px;
margin: 3px 0;
}
`;
@state()
selected: boolean
constructor() {
super();
this.addEventListener('click', this._handleClick.bind(this));
}
render() {
return html`
<div class="menu-item-icon">
${
this.iconUrl ?
html`<img src="${this.iconUrl}" />` :
''
}
</div>
<div class="menu-item-label">
${this.label}
</div>
<div class="menu-item-panel">
<slot></slot>
</div>
`
}
_handleClick() {
if (this.selected) {
this.unselect();
} else {
this.select();
}
}
select() {
this.selected = true;
const event = new CustomEvent(EVENT_MENU_ITEM_SELECTED, {
bubbles: true,
composed: true,
detail: {
element: this,
}
});
this.dispatchEvent(event);
}
unselect() {
this.selected = false;
const event = new CustomEvent(EVENT_MENU_ITEM_UNSELECTED, {
bubbles: true,
composed: true,
detail: {
element: this,
}
});
this.dispatchEvent(event);
}
}

View File

@ -0,0 +1,63 @@
import { LitElement, html, css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { LinkIcon } from './icons';
export class MenuSubItem extends LitElement {
@property({ attribute: 'label' })
label: string;
@property({ attribute: 'icon-url' })
iconUrl: string;
@property({ attribute: 'link-url' })
linkUrl: string;
@property({ attribute: 'inactive', type: Boolean })
inactive: boolean;
static styles = css`
:host {
display: block;
flex: 1;
cursor: pointer;
transition: all 100ms ease-out;
border-bottom: 1px solid rgb(229,231,235);
padding: 5px 0 5px 7px;
border-left: 5px solid transparent;
}
:host([inactive]) {
cursor: initial;
}
:host(:hover) {
border-left: 5px solid #03A9F4;
background-color: rgb(28 169 247 / 10%);
}
a {
font-size: 20px;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
text-decoration: none;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
height: 40px;
color: black;
}
.edge-menu-sub-item-icon {
height: 25px;
width: 25px;
}
.edge-menu-sub-item-label {
margin-left: 5px;
}
`;
render() {
return html`
<a href="${this.linkUrl ? this.linkUrl : '#'}">
<img class="edge-menu-sub-item-icon" src="${this.iconUrl ? this.iconUrl : LinkIcon}" />
<span class="edge-menu-sub-item-label">${this.label}</span>
</a>
`
}
}

View File

@ -0,0 +1,239 @@
import { LitElement, html, css } from 'lit';
import { property, queryAll } from 'lit/decorators.js';
import { CloudIcon, HomeIcon, LoginIcon, LinkIcon, MenuIcon, UserCircleIcon, LogoutIcon } from './icons'
import { EVENT_MENU_ITEM_SELECTED, EVENT_MENU_ITEM_UNSELECTED, MenuItem } from './menu-item';
import { MenuSubItem } from './menu-sub-item';
interface Manifest {
id: string
description: string
metadata: { [key: string]: any }
tags: string[]
title: string
version: string
url?: string
}
interface Profile {
sub?: string
preferred_username?: string
iss?: string
edge_role?: string
edge_tenant?: string
edge_entrypoint?: string
}
const BASE_API_URL = '/edge/api/v1';
enum Roles {
visitor = 0,
user = 1,
superuser = 2,
admin = 3,
superadmin = 4
}
export class Menu extends LitElement {
@property({ attribute: 'app-icon-url', type: String })
appIconUrl: string;
@property({ attribute: 'app-title', type: String })
appTitle: string;
@property({ attribute: 'hidden', type: Boolean })
hidden: boolean;
@property()
_apps: Manifest[] = []
@property()
_profile: Profile
static styles = css`
:host {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background-color: #fff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
z-index: 9999;
}
:host([hidden]) {
display: none;
}
`;
@queryAll('edge-menu-item')
_menuItems: NodeListOf<MenuItem>
constructor() {
super();
this.addEventListener(EVENT_MENU_ITEM_SELECTED, this._handleMenuItemSelected.bind(this));
this.addEventListener(EVENT_MENU_ITEM_UNSELECTED, this._handleMenuItemUnselected.bind(this));
this._fetchApps();
this._fetchProfile();
}
render() {
const apps = this._renderApps()
return html`
<edge-menu-item name='menu' label="${ this.appTitle || "App" }" icon-url='${ this.appIconUrl || MenuIcon }'>
<edge-menu-sub-item name='home' label='Home' icon-url='${HomeIcon}' link-url='/'></edge-menu-sub-item>
<slot></slot>
</edge-menu-item>
${ this._renderApps() }
${ this._renderProfile() }
`;
}
_fetchApps() {
return fetch(`${BASE_API_URL}/apps`)
.then(res => res.json())
.then(result => {
if (result.error) {
throw new Error(`Unexpected server error: ${result.error.code}`);
}
return result.data?.manifests || [];
})
.then((manifests: Manifest[]) => {
const promises = manifests.map((m: Manifest) => {
const fetchOptions: RequestInit = {
method: 'POST',
body: JSON.stringify({}),
headers: {
'Content-Type': 'application/json',
}
};
return fetch(`${BASE_API_URL}/apps/${m.id}/url`, fetchOptions)
.then(res => res.json())
.then(result => {
if (result.error) {
throw new Error(`Unexpected server error: ${result.error.code}`);
}
m.url = result.data?.url;
return m;
})
;
});
return Promise.all(promises);
})
.then((manifests: Manifest[]) => {
this._apps = manifests;
})
.catch(err => console.error(err))
}
_fetchProfile() {
return fetch(`${BASE_API_URL}/profile`)
.then(res => res.json())
.then(result => {
if (result.error) {
switch (result.error.code) {
case "unauthorized":
return null;
default:
throw new Error(`Unexpected server error: ${result.error.code}`);
}
}
return result.data?.profile;
})
.then(profile => {
this._profile = profile;
})
.catch(err => console.error(err))
;
}
_renderApps() {
const apps = this._apps
.filter(manifest => this._canAccess(manifest))
.map(manifest => {
const iconUrl = ( ( manifest.url || '') + ( manifest.metadata?.paths?.icon || '' ) ) || LinkIcon;
return html`
<edge-menu-sub-item
name='${ manifest.id }'
label='${ manifest.title }'
icon-url='${ iconUrl }'
link-url='${ manifest.url || '#' }'>
</edge-menu-sub-item>
`
});
return html`
<edge-menu-item name='apps' label='Apps' icon-url='${CloudIcon}'>
${ apps }
</edge-menu-item>
`;
}
_canAccess(manifest: Manifest): boolean {
const currentRole = this._profile?.edge_role || 'visitor';
const minimumRole = manifest.metadata?.minimumRole || 'visitor';
return Roles[currentRole] >= Roles[minimumRole];
}
_renderProfile() {
const profile = this._profile;
return html`
<edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'>
${
profile ?
html`<edge-menu-sub-item name='login' label='Logout' icon-url='${LogoutIcon}' link-url='/edge/auth/logout'></edge-menu-sub-item>` :
html`<edge-menu-sub-item name='login' label='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>`
}
</edge-menu-item>
`;
}
_handleMenuItemSelected(evt: CustomEvent) {
const selectedMenuItem: HTMLElement = evt.detail.element;
selectedMenuItem.classList.add('selected');
selectedMenuItem.classList.remove('unselected');
for (let item, i = 0; (item = this._menuItems[i]); i++) {
if (item === selectedMenuItem) continue;
item.unselect();
item.classList.add('unselected');
}
}
_handleMenuItemUnselected(evt: CustomEvent) {
const unselectedMenuItem: HTMLElement = evt.detail.element;
unselectedMenuItem.classList.remove('selected');
const hasSelectedItem = this.renderRoot.querySelectorAll('edge-menu-item.selected').length !== 0
if (hasSelectedItem) {
return
}
for (let item, i = 0; (item = this._menuItems[i]); i++) {
item.classList.remove('unselected');
}
}
}
declare global {
interface HTMLElementTagNameMap {
"edge-menu": Menu;
"edge-menu-item": MenuItem;
"edge-menu-sub-item": MenuSubItem;
}
}

4
pkg/sdk/client/src/index.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.svg' {
const content: string;
export default content;
}

View File

@ -1,5 +1,16 @@
import { Client } from './client.js';
import { CrossFrameMessenger } from './crossframe-messenger.js';
import './polyfill';
export const client = new Client();
export const crossFrameMessenger = new CrossFrameMessenger();
import { Client as EdgeClient } from './client.js';
import { Menu as MenuElement } from './components/menu.js';
import { MenuItem as MenuItemElement } from './components/menu-item.js';
import { MenuSubItem as MenuSubItemElement } from './components/menu-sub-item.js';
import { CrossFrameMessenger } from './crossframe-messenger.js';
import { MenuManager } from './menu-manager.js';
customElements.define('edge-menu', MenuElement);
customElements.define('edge-menu-item', MenuItemElement);
customElements.define('edge-menu-sub-item', MenuSubItemElement);
export const Client = new EdgeClient();
export const Frame = new CrossFrameMessenger();
export const Menu = new MenuManager();

View File

@ -0,0 +1,144 @@
import { Menu } from "./components/menu"
export interface MenuItem {
label: string
iconUrl: string
linkUrl: string
order: number
}
const EdgeBodyAutoPaddingAttrName = 'edge-auto-padding'
export class MenuManager {
_items: { [name:string]: MenuItem }
_menu: Menu
_appIconUrl: string
_appTitle: string
_hidden: boolean
_previousBodyAutoPadding: string
constructor() {
this._items = {};
this._handleLoad = this._handleLoad.bind(this);
window.addEventListener('load', this._handleLoad);
}
setItem(name: string, label:string, options?: { iconUrl?: string, linkUrl?: string, order?: number }) {
this._items[name] = {
label: label,
iconUrl: options?.iconUrl ? options?.iconUrl : '',
linkUrl: options?.linkUrl ? options?.linkUrl : '#',
order: options?.order ? options?.order : 0,
}
this._render();
return this;
}
removeItem(name: string) {
delete this._items[name];
this._render();
return this;
}
setAppIconUrl(url: string) {
this._appIconUrl = url;
this._render();
return this;
}
setAppTitle(title: string) {
this._appTitle = title;
this._render();
return this;
}
show() {
if (!this._hidden) return;
this._hidden = false;
if (this._previousBodyAutoPadding) {
document.body.setAttribute(EdgeBodyAutoPaddingAttrName, this._previousBodyAutoPadding);
} else {
document.body.removeAttribute(EdgeBodyAutoPaddingAttrName);
}
this._render();
}
hide() {
if (this._hidden) return;
this._hidden = true;
this._previousBodyAutoPadding = document.body.getAttribute(EdgeBodyAutoPaddingAttrName);
document.body.setAttribute(EdgeBodyAutoPaddingAttrName, "false");
this._render();
}
_handleLoad() {
this._init();
}
_init() {
this._initMenu();
this._initGlobalStyle();
}
_initMenu() {
const menu = document.createElement('edge-menu');
document.body.appendChild(menu);
this._menu = menu;
this._render();
}
_initGlobalStyle() {
const style = document.createElement('style');
style.textContent = `
body:not([${EdgeBodyAutoPaddingAttrName}="false"]) {
padding-top: 60px;
}
`;
document.head.appendChild(style);
}
_render() {
if (!this._menu) return;
if (this._hidden) {
this._menu.setAttribute("hidden", "true");
} else {
this._menu.removeAttribute("hidden");
}
if (this._appIconUrl) {
this._menu.setAttribute("app-icon-url", this._appIconUrl);
} else {
this._menu.removeAttribute("app-icon-url");
}
if (this._appTitle) {
this._menu.setAttribute("app-title", this._appTitle);
} else {
this._menu.removeAttribute("app-title");
}
const children: Node[] = [];
const items: MenuItem[] = Object.keys(this._items)
.map(key => ({ name: key, ...this._items[key] }))
.sort((a, b) => a.order - b.order)
;
for (let item: MenuItem, i = 0; (item = items[i]); i++) {
const node = document.createElement('edge-menu-sub-item');
node.label = item.label;
node.iconUrl = item.iconUrl;
node.linkUrl = item.linkUrl;
children.push(node);
}
this._menu.replaceChildren(...children);
}
}

View File

@ -0,0 +1,4 @@
import 'core-js/actual';
import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
import 'lit/polyfill-support.js'
import '@webcomponents/webcomponentsjs/webcomponents-loader.js';

View File

@ -16,7 +16,7 @@ import (
type BlobBucket struct {
name string
getDB getDBFunc
getDB GetDBFunc
closed bool
}
@ -236,7 +236,7 @@ func (b *BlobBucket) withTx(ctx context.Context, fn func(tx *sql.Tx) error) erro
return errors.WithStack(err)
}
if err := withTx(ctx, db, fn); err != nil {
if err := WithTx(ctx, db, fn); err != nil {
return errors.WithStack(err)
}
@ -246,7 +246,7 @@ func (b *BlobBucket) withTx(ctx context.Context, fn func(tx *sql.Tx) error) erro
type blobWriterCloser struct {
id storage.BlobID
bucket string
getDB getDBFunc
getDB GetDBFunc
buf bytes.Buffer
closed bool
}
@ -335,7 +335,7 @@ func (wbc *blobWriterCloser) withTx(ctx context.Context, fn func(tx *sql.Tx) err
return errors.WithStack(err)
}
if err := withTx(ctx, db, fn); err != nil {
if err := WithTx(ctx, db, fn); err != nil {
return errors.WithStack(err)
}
@ -345,7 +345,7 @@ func (wbc *blobWriterCloser) withTx(ctx context.Context, fn func(tx *sql.Tx) err
type blobReaderCloser struct {
id storage.BlobID
bucket string
getDB getDBFunc
getDB GetDBFunc
reader bytes.Reader
once sync.Once
closed bool
@ -444,7 +444,7 @@ func (brc *blobReaderCloser) withTx(ctx context.Context, fn func(tx *sql.Tx) err
return errors.WithStack(err)
}
if err := withTx(ctx, db, fn); err != nil {
if err := WithTx(ctx, db, fn); err != nil {
return errors.WithStack(err)
}

View File

@ -10,7 +10,7 @@ import (
)
type BlobStore struct {
getDB getDBFunc
getDB GetDBFunc
}
// DeleteBucket implements storage.BlobStore
@ -81,7 +81,7 @@ func (s *BlobStore) OpenBucket(ctx context.Context, name string) (storage.BlobBu
func ensureBlobTables(ctx context.Context, db *sql.DB) error {
logger.Debug(ctx, "creating blobs table")
err := withTx(ctx, db, func(tx *sql.Tx) error {
err := WithTx(ctx, db, func(tx *sql.Tx) error {
query := `
CREATE TABLE IF NOT EXISTS blobs (
id TEXT,
@ -114,7 +114,7 @@ func (s *BlobStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error
return errors.WithStack(err)
}
if err := withTx(ctx, db, fn); err != nil {
if err := WithTx(ctx, db, fn); err != nil {
return errors.WithStack(err)
}
@ -122,13 +122,13 @@ func (s *BlobStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error
}
func NewBlobStore(dsn string) *BlobStore {
getDB := newGetDBFunc(dsn, ensureBlobTables)
getDB := NewGetDBFunc(dsn, ensureBlobTables)
return &BlobStore{getDB}
}
func NewBlobStoreWithDB(db *sql.DB) *BlobStore {
getDB := newGetDBFuncFromDB(db, ensureBlobTables)
getDB := NewGetDBFuncFromDB(db, ensureBlobTables)
return &BlobStore{getDB}
}

View File

@ -17,7 +17,7 @@ import (
)
type DocumentStore struct {
getDB getDBFunc
getDB GetDBFunc
}
// Delete implements storage.DocumentStore
@ -269,7 +269,7 @@ func (s *DocumentStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) e
return errors.WithStack(err)
}
if err := withTx(ctx, db, fn); err != nil {
if err := WithTx(ctx, db, fn); err != nil {
return errors.WithStack(err)
}
@ -277,7 +277,7 @@ func (s *DocumentStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) e
}
func ensureTables(ctx context.Context, db *sql.DB) error {
err := withTx(ctx, db, func(tx *sql.Tx) error {
err := WithTx(ctx, db, func(tx *sql.Tx) error {
query := `
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
@ -344,7 +344,7 @@ func withLimitOffsetClause(query string, args []any, limit int, offset int) (str
}
func NewDocumentStore(path string) *DocumentStore {
getDB := newGetDBFunc(path, ensureTables)
getDB := NewGetDBFunc(path, ensureTables)
return &DocumentStore{
getDB: getDB,
@ -352,7 +352,7 @@ func NewDocumentStore(path string) *DocumentStore {
}
func NewDocumentStoreWithDB(db *sql.DB) *DocumentStore {
getDB := newGetDBFuncFromDB(db, ensureTables)
getDB := NewGetDBFuncFromDB(db, ensureTables)
return &DocumentStore{
getDB: getDB,

View File

@ -22,7 +22,7 @@ func Open(path string) (*sql.DB, error) {
return db, nil
}
func withTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
func WithTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
var tx *sql.Tx
tx, err := db.BeginTx(ctx, nil)
@ -70,9 +70,9 @@ func withTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
return nil
}
type getDBFunc func(ctx context.Context) (*sql.DB, error)
type GetDBFunc func(ctx context.Context) (*sql.DB, error)
func newGetDBFunc(dsn string, initFunc func(ctx context.Context, db *sql.DB) error) getDBFunc {
func NewGetDBFunc(dsn string, initFunc func(ctx context.Context, db *sql.DB) error) GetDBFunc {
var (
db *sql.DB
mutex sync.RWMutex
@ -110,7 +110,7 @@ func newGetDBFunc(dsn string, initFunc func(ctx context.Context, db *sql.DB) err
}
}
func newGetDBFuncFromDB(db *sql.DB, initFunc func(ctx context.Context, db *sql.DB) error) getDBFunc {
func NewGetDBFuncFromDB(db *sql.DB, initFunc func(ctx context.Context, db *sql.DB) error) GetDBFunc {
var err error
initOnce := &sync.Once{}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"lib": ["ES2015", "DOM"],
"experimentalDecorators": true
},
"include": ["pkg/sdk/client/src/index.d.ts", "pkg/sdk/client/src/**/*.ts", "pkg/sdk/client/src/**/*.svg"]
}