Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
fefcba5901 | |||
5ad4ab2e23 | |||
4eb1f8fc90 | |||
640f429580 | |||
7f07e52ae0 | |||
c4865c149f | |||
b9f985ab0c | |||
f01b1ef3b2 | |||
c721d46218 | |||
a13dfffd5c | |||
19cd4d56e7 | |||
85f50eb9d5 | |||
673586c2f7 | |||
4606d7a08d | |||
3e601272d7 |
@ -125,23 +125,34 @@ func copyDir(writer *zip.Writer, baseDir string, zipBasePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(writer *zip.Writer, srcPath string, zipPath string) error {
|
func copyFile(writer *zip.Writer, srcPath string, zipPath string) error {
|
||||||
r, err := os.Open(srcPath)
|
srcFile, err := os.Open(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcStat, err := os.Stat(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := r.Close(); err != nil {
|
if err := srcFile.Close(); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
f, err := writer.Create(zipPath)
|
fileHeader := &zip.FileHeader{
|
||||||
|
Name: zipPath,
|
||||||
|
Modified: srcStat.ModTime().UTC(),
|
||||||
|
Method: zip.Deflate,
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := writer.CreateHeader(fileHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = io.Copy(f, r); err != nil {
|
if _, err = io.Copy(file, srcFile); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,18 +2,28 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||||
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||||
|
"github.com/dop251/goja"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
@ -52,14 +62,35 @@ func RunCommand() *cli.Command {
|
|||||||
Usage: "use `FILE` for SQLite storage database",
|
Usage: "use `FILE` for SQLite storage database",
|
||||||
Value: "data.sqlite",
|
Value: "data.sqlite",
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth-subject",
|
||||||
|
Usage: "set the `SUBJECT` associated with the simulated connected user",
|
||||||
|
Value: "jdoe",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth-role",
|
||||||
|
Usage: "set the `ROLE` associated with the simulated connected user",
|
||||||
|
Value: "user",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth-preferred-username",
|
||||||
|
Usage: "set the `PREFERRED_USERNAME` associated with the simulated connected user",
|
||||||
|
Value: "Jane Doe",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
address := ctx.String("address")
|
address := ctx.String("address")
|
||||||
path := ctx.String("path")
|
path := ctx.String("path")
|
||||||
|
|
||||||
logFormat := ctx.String("log-format")
|
logFormat := ctx.String("log-format")
|
||||||
logLevel := ctx.Int("log-level")
|
logLevel := ctx.Int("log-level")
|
||||||
|
|
||||||
storageFile := ctx.String("storage-file")
|
storageFile := ctx.String("storage-file")
|
||||||
|
|
||||||
|
authSubject := ctx.String("auth-subject")
|
||||||
|
authRole := ctx.String("auth-role")
|
||||||
|
authPreferredUsername := ctx.String("auth-preferred-username")
|
||||||
|
|
||||||
logger.SetFormat(logger.Format(logFormat))
|
logger.SetFormat(logger.Format(logFormat))
|
||||||
logger.SetLevel(logger.Level(logLevel))
|
logger.SetLevel(logger.Level(logLevel))
|
||||||
|
|
||||||
@ -80,6 +111,7 @@ func RunCommand() *cli.Command {
|
|||||||
mux := chi.NewMux()
|
mux := chi.NewMux()
|
||||||
|
|
||||||
mux.Use(middleware.Logger)
|
mux.Use(middleware.Logger)
|
||||||
|
mux.Use(dummyAuthMiddleware(authSubject, authRole, authPreferredUsername))
|
||||||
|
|
||||||
bus := memory.NewBus()
|
bus := memory.NewBus()
|
||||||
|
|
||||||
@ -88,20 +120,12 @@ func RunCommand() *cli.Command {
|
|||||||
return errors.Wrapf(err, "could not open database with path '%s'", storageFile)
|
return errors.Wrapf(err, "could not open database with path '%s'", storageFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
documentStore := sqlite.NewDocumentStoreWithDB(db)
|
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||||
blobStore := sqlite.NewBlobStoreWithDB(db)
|
bs := sqlite.NewBlobStoreWithDB(db)
|
||||||
|
|
||||||
handler := appHTTP.NewHandler(
|
handler := appHTTP.NewHandler(
|
||||||
appHTTP.WithBus(bus),
|
appHTTP.WithBus(bus),
|
||||||
appHTTP.WithServerModules(
|
appHTTP.WithServerModules(getServerModules(bus, ds, bs)...),
|
||||||
module.ContextModuleFactory(),
|
|
||||||
module.ConsoleModuleFactory(),
|
|
||||||
module.LifecycleModuleFactory(bus),
|
|
||||||
module.NetModuleFactory(bus),
|
|
||||||
module.RPCModuleFactory(bus),
|
|
||||||
module.StoreModuleFactory(documentStore),
|
|
||||||
module.BlobModuleFactory(bus, blobStore),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if err := handler.Load(bundle); err != nil {
|
if err := handler.Load(bundle); err != nil {
|
||||||
return errors.Wrap(err, "could not load app bundle")
|
return errors.Wrap(err, "could not load app bundle")
|
||||||
@ -119,3 +143,89 @@ func RunCommand() *cli.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore) []app.ServerModuleFactory {
|
||||||
|
return []app.ServerModuleFactory{
|
||||||
|
module.ContextModuleFactory(),
|
||||||
|
module.ConsoleModuleFactory(),
|
||||||
|
cast.CastModuleFactory(),
|
||||||
|
module.LifecycleModuleFactory(),
|
||||||
|
net.ModuleFactory(bus),
|
||||||
|
module.RPCModuleFactory(bus),
|
||||||
|
module.StoreModuleFactory(ds),
|
||||||
|
module.BlobModuleFactory(bus, bs),
|
||||||
|
module.Extends(
|
||||||
|
auth.ModuleFactory(
|
||||||
|
auth.WithJWT(dummyKeyFunc),
|
||||||
|
),
|
||||||
|
func(o *goja.Object) {
|
||||||
|
if err := o.Set("CLAIM_ROLE", "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"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dummySecret = []byte("not_so_secret")
|
||||||
|
|
||||||
|
func dummyKeyFunc(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return dummySecret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dummyAuthMiddleware(subject, role, username string) func(http.Handler) http.Handler {
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
unauthenticated := subject == "" && role == "" && username == ""
|
||||||
|
|
||||||
|
if unauthenticated {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"nbf": time.Now().UTC().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if subject != "" {
|
||||||
|
claims["sub"] = subject
|
||||||
|
}
|
||||||
|
|
||||||
|
if role != "" {
|
||||||
|
claims["role"] = role
|
||||||
|
}
|
||||||
|
|
||||||
|
if username != "" {
|
||||||
|
claims["preferred_username"] = username
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
rawToken, err := token.SignedString(dummySecret)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not sign token", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Add("Authorization", "Bearer "+rawToken)
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ Comme son nom l'indique, elle permet d'exécuter des opérations d'initialisatio
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
function onInit() {
|
function onInit() {
|
||||||
|
console.log("My app booted !")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -20,9 +20,11 @@ function onInit() {
|
|||||||
|
|
||||||
Listes des modules disponibles côté serveur.
|
Listes des modules disponibles côté serveur.
|
||||||
|
|
||||||
|
- [`auth`](./auth.md)
|
||||||
|
- [`blob`](./blob.md)
|
||||||
|
- [`cast`](./cast.md)
|
||||||
- [`console`](./console.md)
|
- [`console`](./console.md)
|
||||||
- [`context`](./context.md)
|
- [`context`](./context.md)
|
||||||
- [`net`](./net.md)
|
- [`net`](./net.md)
|
||||||
- [`rpc`](./rpc.md)
|
- [`rpc`](./rpc.md)
|
||||||
- [`store`](./store.md)
|
- [`store`](./store.md)
|
||||||
- [`blob`](./blob.md)
|
|
41
doc/apps/server-api/auth.md
Normal file
41
doc/apps/server-api/auth.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Module `auth`
|
||||||
|
|
||||||
|
Ce module permet de récupérer des informations concernant l'utilisateur connecté et ses attributs.
|
||||||
|
|
||||||
|
## Méthodes
|
||||||
|
|
||||||
|
### `auth.getClaim(ctx: Context, name: string): string`
|
||||||
|
|
||||||
|
Récupère un attribut associé à l'utilisateur.
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
|
||||||
|
- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md)
|
||||||
|
- `name` **string** Nom de l'attribut à retrouver
|
||||||
|
|
||||||
|
#### Valeur de retour
|
||||||
|
|
||||||
|
Valeur de l'attribut associé ou vide si la requête est non authentifiée ou que l'attribut n'a pas été trouvé.
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
function onClientMessage(ctx, message) {
|
||||||
|
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
|
||||||
|
console.log("Connected user is", subject);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Propriétés
|
||||||
|
|
||||||
|
### `auth.CLAIM_SUBJECT`
|
||||||
|
|
||||||
|
Cette propriété identifie l'utilisateur connecté. Si la valeur retournée par la méthode `getClaim()` est vide, alors l'utilisateur n'est pas connecté.
|
||||||
|
|
||||||
|
### `auth.CLAIM_ROLE`
|
||||||
|
|
||||||
|
Cette propriété retourne le rôle de l'utilisateur connecté au sein du "tenant" courant. Si la valeur retournée par la méthode `getClaim()` est vide, alors l'utilisateur n'est pas connecté.
|
||||||
|
|
||||||
|
### `auth.CLAIM_PREFERRED_USERNAME`
|
||||||
|
|
||||||
|
Cette propriété retourne le nom "préféré pour l'affichage" de l'utilisateur connecté. Si la valeur retournée par la méthode `getClaim()` est vide, alors l'utilisateur n'est pas connecté ou l'utilisateur n'a pas défini de nom d'utilisateur particulier.
|
38
doc/apps/server-api/cast.md
Normal file
38
doc/apps/server-api/cast.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Module `cast`
|
||||||
|
|
||||||
|
Ce module permet de communiquer avec des appareils de présentation de type [Chromecast](https://store.google.com/fr/product/chromecast_setup?hl=fr).
|
||||||
|
|
||||||
|
## Méthodes
|
||||||
|
|
||||||
|
### `cast.refreshDevices(timeout?: string = '30s'): Promise<Device[]>`
|
||||||
|
|
||||||
|
Rafraichit la liste locale des appareils de présentation disponibles sur les réseaux locaux de la borne.
|
||||||
|
|
||||||
|
L'appel à cette méthode rafraîchit également la liste mise en cache et renvoyée par `cast.getDevices()`.
|
||||||
|
|
||||||
|
### `cast.getDevices(): []Device`
|
||||||
|
|
||||||
|
Retourne la liste mise en cache des appareils de présentation disponibles sur les réseaux locaux de la borne.
|
||||||
|
|
||||||
|
La liste est initialement vide. Un appel initial à `cast.refreshDevices()` est nécessaire afin de mettre à jour celle ci.
|
||||||
|
|
||||||
|
### `cast.loadUrl(deviceUuid: string, url: string, timeout?: string = '30s'): Promise<void>`
|
||||||
|
|
||||||
|
Charge l'URL donnée sur l'appareil de présentation identifié par l'UUID `deviceUuid`.
|
||||||
|
|
||||||
|
### `cast.stopCast(deviceUuid: string, timeout?: string = '30s'): Promise<void>`
|
||||||
|
|
||||||
|
Stoppe l'application courante sur l'appareil de présentation identifié par l'UUID `deviceUuid`.
|
||||||
|
|
||||||
|
## Objets
|
||||||
|
|
||||||
|
### `Device`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Device {
|
||||||
|
uuid: string // UUID de l'appareil
|
||||||
|
name: string // Nom de l'appareil
|
||||||
|
host: string // Adresse IPv4 de l'appareil
|
||||||
|
port: number // Port distant du service
|
||||||
|
}
|
||||||
|
```
|
9
go.mod
9
go.mod
@ -4,9 +4,18 @@ go 1.19
|
|||||||
|
|
||||||
require modernc.org/sqlite v1.20.4
|
require modernc.org/sqlite v1.20.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
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/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cdr.dev/slog v1.4.0 // indirect
|
cdr.dev/slog v1.4.0 // indirect
|
||||||
github.com/alecthomas/chroma v0.7.0 // indirect
|
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/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // 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 // indirect
|
||||||
|
17
go.sum
17
go.sum
@ -52,6 +52,8 @@ github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYU
|
|||||||
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/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/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
|
||||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
github.com/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
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/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
@ -66,6 +68,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||||||
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
|
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
|
||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||||
|
github.com/davecgh/go-spew v1.0.1-0.20160907170601-6d212800a42e/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@ -105,6 +108,10 @@ github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3yg
|
|||||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
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 h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
|
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/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-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@ -172,9 +179,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/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 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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.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/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/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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/igm/sockjs-go/v3 v3.0.2 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE=
|
github.com/igm/sockjs-go/v3 v3.0.2 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE=
|
||||||
@ -202,6 +213,8 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe
|
|||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
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 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
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/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
@ -217,6 +230,7 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/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 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||||
@ -229,11 +243,13 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
|
|||||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.1.5-0.20160925220609-976c720a22c8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||||
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
|
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
|
||||||
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
@ -299,6 +315,7 @@ 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.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<script src="/edge/sdk/client.js"></script>
|
<script src="/edge/sdk/client.js"></script>
|
||||||
<script src="test/client-sdk.js"></script>
|
<script src="test/client-sdk.js"></script>
|
||||||
|
<script src="test/auth-module.js"></script>
|
||||||
<script class="mocha-exec">
|
<script class="mocha-exec">
|
||||||
mocha.run();
|
mocha.run();
|
||||||
</script>
|
</script>
|
||||||
|
20
misc/client-sdk-testsuite/src/public/test/auth-module.js
Normal file
20
misc/client-sdk-testsuite/src/public/test/auth-module.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
describe('Auth Module', function() {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
return Edge.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
Edge.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve user informations', function() {
|
||||||
|
return Edge.rpc("getUserInfo")
|
||||||
|
.then(userInfo => {
|
||||||
|
chai.assert.isNotNull(userInfo.subject);
|
||||||
|
chai.assert.isNotNull(userInfo.role);
|
||||||
|
chai.assert.isNotNull(userInfo.preferredUsername);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -10,12 +10,12 @@ function onInit() {
|
|||||||
rpc.register("add", add);
|
rpc.register("add", add);
|
||||||
rpc.register("reset", reset);
|
rpc.register("reset", reset);
|
||||||
rpc.register("total", total);
|
rpc.register("total", total);
|
||||||
|
rpc.register("getUserInfo", getUserInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called for each client message
|
// Called for each client message
|
||||||
function onClientMessage(ctx, data) {
|
function onClientMessage(ctx, data) {
|
||||||
var sessionId = context.get(ctx, context.SESSION_ID);
|
console.log("onClientMessage", data.now);
|
||||||
console.log("onClientMessage", sessionId, data.now);
|
|
||||||
net.send(ctx, { now: data.now });
|
net.send(ctx, { now: data.now });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,3 +61,15 @@ function reset(ctx, params) {
|
|||||||
function total(ctx, params) {
|
function total(ctx, params) {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUserInfo(ctx, params) {
|
||||||
|
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
|
||||||
|
var role = auth.getClaim(ctx, auth.CLAIM_ROLE);
|
||||||
|
var preferredUsername = auth.getClaim(ctx, auth.CLAIM_PREFERRED_USERNAME);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject: subject,
|
||||||
|
role: role,
|
||||||
|
preferredUsername: preferredUsername,
|
||||||
|
};
|
||||||
|
}
|
@ -145,7 +145,7 @@ func (s *Server) Stop() {
|
|||||||
func (s *Server) initModules(factories ...ServerModuleFactory) {
|
func (s *Server) initModules(factories ...ServerModuleFactory) {
|
||||||
runtime := goja.New()
|
runtime := goja.New()
|
||||||
|
|
||||||
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("goja", true))
|
||||||
runtime.SetRandSource(createRandomSource())
|
runtime.SetRandSource(createRandomSource())
|
||||||
|
|
||||||
modules := make([]ServerModule, 0, len(factories))
|
modules := make([]ServerModule, 0, len(factories))
|
||||||
|
@ -65,7 +65,7 @@ func (h *Handler) handleAppUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
||||||
module.ContextKeyOriginRequest: r,
|
ContextKeyOriginRequest: r,
|
||||||
})
|
})
|
||||||
|
|
||||||
requestMsg := module.NewMessageUploadRequest(ctx, fileHeader, metadata)
|
requestMsg := module.NewMessageUploadRequest(ctx, fileHeader, metadata)
|
||||||
@ -117,7 +117,7 @@ func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
|
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
||||||
module.ContextKeyOriginRequest: r,
|
ContextKeyOriginRequest: r,
|
||||||
})
|
})
|
||||||
|
|
||||||
requestMsg := module.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID))
|
requestMsg := module.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID))
|
||||||
|
@ -26,7 +26,7 @@ func defaultHandlerOptions() *HandlerOptions {
|
|||||||
Bus: memory.NewBus(),
|
Bus: memory.NewBus(),
|
||||||
SockJS: sockjsOptions,
|
SockJS: sockjsOptions,
|
||||||
ServerModuleFactories: make([]app.ServerModuleFactory, 0),
|
ServerModuleFactories: make([]app.ServerModuleFactory, 0),
|
||||||
UploadMaxFileSize: 1024 * 10, // 10Mb
|
UploadMaxFileSize: 10 << (10 * 2), // 10Mb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,11 @@ const (
|
|||||||
statusChannelClosed = iota
|
statusChannelClosed = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContextKeySessionID module.ContextKey = "sessionId"
|
||||||
|
ContextKeyOriginRequest module.ContextKey = "originRequest"
|
||||||
|
)
|
||||||
|
|
||||||
func (h *Handler) handleSockJS(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleSockJS(w http.ResponseWriter, r *http.Request) {
|
||||||
h.mutex.RLock()
|
h.mutex.RLock()
|
||||||
defer h.mutex.RUnlock()
|
defer h.mutex.RUnlock()
|
||||||
@ -79,7 +84,7 @@ func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := module.ContextValue[string](serverMessage.Context, module.ContextKeySessionID)
|
sessionID := module.ContextValue[string](serverMessage.Context, ContextKeySessionID)
|
||||||
|
|
||||||
isDest := sessionID == "" || sessionID == sess.ID()
|
isDest := sessionID == "" || sessionID == sess.ID()
|
||||||
if !isDest {
|
if !isDest {
|
||||||
@ -182,8 +187,8 @@ func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session)
|
|||||||
|
|
||||||
ctx := logger.With(ctx, logger.F("payload", payload))
|
ctx := logger.With(ctx, logger.F("payload", payload))
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
||||||
module.ContextKeySessionID: sess.ID(),
|
ContextKeySessionID: sess.ID(),
|
||||||
module.ContextKeyOriginRequest: sess.Request(),
|
ContextKeyOriginRequest: sess.Request(),
|
||||||
})
|
})
|
||||||
|
|
||||||
clientMessage := module.NewClientMessage(ctx, payload)
|
clientMessage := module.NewClientMessage(ctx, payload)
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
package module
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
func assertType[T any](v goja.Value, rt *goja.Runtime) T {
|
|
||||||
if c, ok := v.Export().(T); ok {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(rt.NewTypeError(fmt.Sprintf("expected value to be a '%T', got '%T'", new(T), v.Export())))
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertContext(v goja.Value, r *goja.Runtime) context.Context {
|
|
||||||
return assertType[context.Context](v, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertObject(v goja.Value, r *goja.Runtime) map[string]any {
|
|
||||||
return assertType[map[string]any](v, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertString(v goja.Value, r *goja.Runtime) string {
|
|
||||||
return assertType[string](v, r)
|
|
||||||
}
|
|
8
pkg/module/auth/error.go
Normal file
8
pkg/module/auth/error.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnauthenticated = errors.New("unauthenticated")
|
||||||
|
ErrClaimNotFound = errors.New("claim not found")
|
||||||
|
)
|
60
pkg/module/auth/jwt.go
Normal file
60
pkg/module/auth/jwt.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithJWT(keyFunc jwt.Keyfunc) OptionFunc {
|
||||||
|
return func(o *Option) {
|
||||||
|
o.GetClaim = func(ctx context.Context, r *http.Request, claimName string) (string, error) {
|
||||||
|
claim, err := getClaim[string](r, claimName, keyFunc)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return claim, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClaim[T any](r *http.Request, claimAttr string, keyFunc jwt.Keyfunc) (T, error) {
|
||||||
|
rawToken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||||
|
if rawToken == "" {
|
||||||
|
rawToken = r.URL.Query().Get("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawToken == "" {
|
||||||
|
return *new(T), errors.WithStack(ErrUnauthenticated)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.Parse(rawToken, keyFunc)
|
||||||
|
if err != nil {
|
||||||
|
return *new(T), errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
return *new(T), errors.Errorf("invalid jwt token: '%v'", token.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return *new(T), errors.Errorf("unexpected claims type '%T'", token.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawClaim, exists := mapClaims[claimAttr]
|
||||||
|
if !exists {
|
||||||
|
return *new(T), errors.WithStack(ErrClaimNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 claim, nil
|
||||||
|
}
|
69
pkg/module/auth/module.go
Normal file
69
pkg/module/auth/module.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClaimSubject = "sub"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module struct {
|
||||||
|
server *app.Server
|
||||||
|
getClaimFunc GetClaimFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Name() string {
|
||||||
|
return "auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Export(export *goja.Object) {
|
||||||
|
if err := export.Set("getClaim", m.getClaim); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'getClaim' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("CLAIM_SUBJECT", ClaimSubject); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'CLAIM_SUBJECT' property"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
|
claimName := util.AssertString(call.Argument(1), rt)
|
||||||
|
|
||||||
|
req, ok := ctx.Value(edgeHTTP.ContextKeyOriginRequest).(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
panic(rt.ToValue(errors.New("could not find http request in context")))
|
||||||
|
}
|
||||||
|
|
||||||
|
claim, err := m.getClaimFunc(ctx, req, claimName)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrUnauthenticated) || errors.Is(err, ErrClaimNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return rt.ToValue(claim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
|
||||||
|
opt := &Option{}
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(server *app.Server) app.ServerModule {
|
||||||
|
return &Module{
|
||||||
|
server: server,
|
||||||
|
getClaimFunc: opt.GetClaim,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
pkg/module/auth/module_test.go
Normal file
124
pkg/module/auth/module_test.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cdr.dev/slog"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthModule(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
|
||||||
|
keyFunc, secret := getKeyFunc()
|
||||||
|
|
||||||
|
server := app.NewServer(
|
||||||
|
module.ConsoleModuleFactory(),
|
||||||
|
ModuleFactory(
|
||||||
|
WithJWT(keyFunc),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile("testdata/auth.js")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Load("testdata/auth.js", string(data)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "/foo", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": "jdoe",
|
||||||
|
"nbf": time.Now().UTC().Unix(),
|
||||||
|
})
|
||||||
|
|
||||||
|
rawToken, err := token.SignedString(secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Authorization", "Bearer "+rawToken)
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), edgeHTTP.ContextKeyOriginRequest, req)
|
||||||
|
|
||||||
|
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthAnonymousModule(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
|
||||||
|
keyFunc, _ := getKeyFunc()
|
||||||
|
|
||||||
|
server := app.NewServer(
|
||||||
|
module.ConsoleModuleFactory(),
|
||||||
|
ModuleFactory(WithJWT(keyFunc)),
|
||||||
|
)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Load("testdata/auth_anonymous.js", string(data)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "/foo", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), edgeHTTP.ContextKeyOriginRequest, req)
|
||||||
|
|
||||||
|
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyFunc() (jwt.Keyfunc, []byte) {
|
||||||
|
secret := []byte("not_so_secret")
|
||||||
|
|
||||||
|
keyFunc := func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyFunc, secret
|
||||||
|
}
|
20
pkg/module/auth/option.go
Normal file
20
pkg/module/auth/option.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetClaimFunc func(ctx context.Context, r *http.Request, claimName string) (string, error)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
GetClaim GetClaimFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionFunc func(*Option)
|
||||||
|
|
||||||
|
func WithGetClaim(fn GetClaimFunc) OptionFunc {
|
||||||
|
return func(o *Option) {
|
||||||
|
o.GetClaim = fn
|
||||||
|
}
|
||||||
|
}
|
9
pkg/module/auth/testdata/auth.js
vendored
Normal file
9
pkg/module/auth/testdata/auth.js
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
function testAuth(ctx) {
|
||||||
|
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
|
||||||
|
|
||||||
|
if (subject !== "jdoe") {
|
||||||
|
throw new Error("subject: expected 'jdoe', got '"+subject+"'");
|
||||||
|
}
|
||||||
|
}
|
9
pkg/module/auth/testdata/auth_anonymous.js
vendored
Normal file
9
pkg/module/auth/testdata/auth_anonymous.js
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
function testAuth(ctx) {
|
||||||
|
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
|
||||||
|
|
||||||
|
if (subject !== undefined) {
|
||||||
|
throw new Error("subject: expected undefined, got '"+subject+"'");
|
||||||
|
}
|
||||||
|
}
|
@ -1,109 +0,0 @@
|
|||||||
package module
|
|
||||||
|
|
||||||
// import (
|
|
||||||
// "context"
|
|
||||||
// "sync"
|
|
||||||
|
|
||||||
// "forge.cadoles.com/arcad/edge/pkg/app"
|
|
||||||
// "forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
// "forge.cadoles.com/arcad/edge/pkg/repository"
|
|
||||||
// "github.com/dop251/goja"
|
|
||||||
// "github.com/pkg/errors"
|
|
||||||
// "gitlab.com/wpetit/goweb/logger"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// type AuthorizationModule struct {
|
|
||||||
// appID app.ID
|
|
||||||
// bus bus.Bus
|
|
||||||
// backend *app.Server
|
|
||||||
// admins sync.Map
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *AuthorizationModule) Name() string {
|
|
||||||
// return "authorization"
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *AuthorizationModule) Export(export *goja.Object) {
|
|
||||||
// if err := export.Set("isAdmin", m.isAdmin); err != nil {
|
|
||||||
// panic(errors.Wrap(err, "could not set 'register' function"))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *AuthorizationModule) isAdmin(call goja.FunctionCall) goja.Value {
|
|
||||||
// userID := call.Argument(0).String()
|
|
||||||
// if userID == "" {
|
|
||||||
// panic(errors.New("first argument must be a user id"))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// rawValue, exists := m.admins.Load(repository.UserID(userID))
|
|
||||||
// if !exists {
|
|
||||||
// return m.backend.ToValue(false)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// isAdmin, ok := rawValue.(bool)
|
|
||||||
// if !ok {
|
|
||||||
// return m.backend.ToValue(false)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return m.backend.ToValue(isAdmin)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *AuthorizationModule) handleEvents() {
|
|
||||||
// ctx := logger.With(context.Background(), logger.F("moduleAppID", m.appID))
|
|
||||||
|
|
||||||
// ns := AppMessageNamespace(m.appID)
|
|
||||||
|
|
||||||
// userConnectedMessages, err := m.bus.Subscribe(ctx, ns, MessageTypeUserConnected)
|
|
||||||
// if err != nil {
|
|
||||||
// panic(errors.WithStack(err))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// userDisconnectedMessages, err := m.bus.Subscribe(ctx, ns, MessageTypeUserDisconnected)
|
|
||||||
// if err != nil {
|
|
||||||
// panic(errors.WithStack(err))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// defer func() {
|
|
||||||
// m.bus.Unsubscribe(ctx, ns, MessageTypeUserConnected, userConnectedMessages)
|
|
||||||
// m.bus.Unsubscribe(ctx, ns, MessageTypeUserDisconnected, userDisconnectedMessages)
|
|
||||||
// }()
|
|
||||||
|
|
||||||
// for {
|
|
||||||
// select {
|
|
||||||
// case msg := <-userConnectedMessages:
|
|
||||||
// userConnectedMsg, ok := msg.(*MessageUserConnected)
|
|
||||||
// if !ok {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// logger.Debug(ctx, "user connected", logger.F("msg", userConnectedMsg))
|
|
||||||
|
|
||||||
// m.admins.Store(userConnectedMsg.UserID, userConnectedMsg.IsAdmin)
|
|
||||||
|
|
||||||
// case msg := <-userDisconnectedMessages:
|
|
||||||
// userDisconnectedMsg, ok := msg.(*MessageUserDisconnected)
|
|
||||||
// if !ok {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// logger.Debug(ctx, "user disconnected", logger.F("msg", userDisconnectedMsg))
|
|
||||||
|
|
||||||
// m.admins.Delete(userDisconnectedMsg.UserID)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func AuthorizationModuleFactory(b bus.Bus) app.ServerModuleFactory {
|
|
||||||
// return func(appID app.ID, backend *app.Server) app.ServerModule {
|
|
||||||
// mod := &AuthorizationModule{
|
|
||||||
// appID: appID,
|
|
||||||
// bus: b,
|
|
||||||
// backend: backend,
|
|
||||||
// admins: sync.Map{},
|
|
||||||
// }
|
|
||||||
|
|
||||||
// go mod.handleEvents()
|
|
||||||
|
|
||||||
// return mod
|
|
||||||
// }
|
|
||||||
// }
|
|
@ -1,103 +0,0 @@
|
|||||||
package module
|
|
||||||
|
|
||||||
// import (
|
|
||||||
// "context"
|
|
||||||
// "io/ioutil"
|
|
||||||
// "testing"
|
|
||||||
// "time"
|
|
||||||
|
|
||||||
// "forge.cadoles.com/arcad/edge/pkg/app"
|
|
||||||
// "forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// func TestAuthorizationModule(t *testing.T) {
|
|
||||||
// t.Parallel()
|
|
||||||
|
|
||||||
// testAppID := app.ID("test-app")
|
|
||||||
|
|
||||||
// b := memory.NewBus()
|
|
||||||
|
|
||||||
// backend := app.NewServer(testAppID,
|
|
||||||
// ConsoleModuleFactory(),
|
|
||||||
// AuthorizationModuleFactory(b),
|
|
||||||
// )
|
|
||||||
|
|
||||||
// data, err := ioutil.ReadFile("testdata/authorization.js")
|
|
||||||
// if err != nil {
|
|
||||||
// t.Fatal(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if err := backend.Load(string(data)); err != nil {
|
|
||||||
// t.Fatal(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// backend.Start()
|
|
||||||
// defer backend.Stop()
|
|
||||||
|
|
||||||
// if err := backend.OnInit(); err != nil {
|
|
||||||
// t.Error(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Test non connected user
|
|
||||||
|
|
||||||
// retValue, err := backend.ExecFuncByName("isAdmin", testUserID)
|
|
||||||
// if err != nil {
|
|
||||||
// t.Error(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// isAdmin := retValue.ToBoolean()
|
|
||||||
|
|
||||||
// if e, g := false, isAdmin; e != g {
|
|
||||||
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Test user connection as normal user
|
|
||||||
|
|
||||||
// ctx := context.Background()
|
|
||||||
|
|
||||||
// b.Publish(ctx, NewMessageUserConnected(testAppID, testUserID, false))
|
|
||||||
// time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
// retValue, err = backend.ExecFuncByName("isAdmin", testUserID)
|
|
||||||
// if err != nil {
|
|
||||||
// t.Error(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// isAdmin = retValue.ToBoolean()
|
|
||||||
|
|
||||||
// if e, g := false, isAdmin; e != g {
|
|
||||||
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Test user connection as admin
|
|
||||||
|
|
||||||
// b.Publish(ctx, NewMessageUserConnected(testAppID, testUserID, true))
|
|
||||||
// time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
// retValue, err = backend.ExecFuncByName("isAdmin", testUserID)
|
|
||||||
// if err != nil {
|
|
||||||
// t.Error(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// isAdmin = retValue.ToBoolean()
|
|
||||||
|
|
||||||
// if e, g := true, isAdmin; e != g {
|
|
||||||
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Test user disconnection
|
|
||||||
|
|
||||||
// b.Publish(ctx, NewMessageUserDisconnected(testAppID, testUserID))
|
|
||||||
// time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
// retValue, err = backend.ExecFuncByName("isAdmin", testUserID)
|
|
||||||
// if err != nil {
|
|
||||||
// t.Error(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// isAdmin = retValue.ToBoolean()
|
|
||||||
|
|
||||||
// if e, g := false, isAdmin; e != g {
|
|
||||||
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
|
|
||||||
// }
|
|
||||||
// }
|
|
209
pkg/module/cast/cast.go
Normal file
209
pkg/module/cast/cast.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
package cast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/barnybug/go-cast"
|
||||||
|
"github.com/barnybug/go-cast/discovery"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Device struct {
|
||||||
|
UUID string `goja:"uuid" json:"uuid"`
|
||||||
|
Host net.IP `goja:"host" json:"host"`
|
||||||
|
Port int `goja:"port" json:"port"`
|
||||||
|
Name string `goja:"name" json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceStatus struct {
|
||||||
|
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
|
||||||
|
Volume DeviceStatusVolume `goja:"volume" json:"volume"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceStatusCurrentApp struct {
|
||||||
|
ID string `goja:"id" json:"id"`
|
||||||
|
DisplayName string `goja:"displayName" json:"displayName"`
|
||||||
|
StatusText string `goja:"statusText" json:"statusText"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceStatusVolume struct {
|
||||||
|
Level float64 `goja:"level" json:"level"`
|
||||||
|
Muted bool `goja:"muted" json:"muted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceDiscoveryPollingInterval time.Duration = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
|
||||||
|
device, err := findDeviceByUUID(ctx, uuid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := cast.NewClient(device.Host, device.Port)
|
||||||
|
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
break LOOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.WithStack(ErrDeviceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findDevices(ctx context.Context) ([]*Device, error) {
|
||||||
|
service := discovery.NewService(ctx)
|
||||||
|
defer service.Stop()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
devices = append(devices, &Device{
|
||||||
|
Host: c.IP().To4(),
|
||||||
|
Port: c.Port(),
|
||||||
|
Name: c.Name(),
|
||||||
|
UUID: c.Uuid(),
|
||||||
|
})
|
||||||
|
found[c.Uuid()] = struct{}{}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
break LOOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.Err(); err != nil && !errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadURL(ctx context.Context, deviceUUID string, url string) error {
|
||||||
|
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Connect(ctx); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
controller, err := client.URL(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore context.DeadlineExceeded errors. github.com/barnybug/go-cast bug ?
|
||||||
|
if _, err := controller.LoadURL(ctx, url); err != nil && !isLoadURLContextExceeded(err) {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// False positive workaround.
|
||||||
|
func isLoadURLContextExceeded(err error) bool {
|
||||||
|
return err.Error() == "Failed to send load command: context deadline exceeded"
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCast(ctx context.Context, deviceUUID string) error {
|
||||||
|
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Connect(ctx); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if _, err := client.Receiver().QuitApp(ctx); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) {
|
||||||
|
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Connect(ctx); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
ctrlStatus, err := client.Receiver().GetStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := &DeviceStatus{
|
||||||
|
CurrentApp: DeviceStatusCurrentApp{
|
||||||
|
ID: "",
|
||||||
|
DisplayName: "",
|
||||||
|
StatusText: "",
|
||||||
|
},
|
||||||
|
Volume: DeviceStatusVolume{
|
||||||
|
Level: *ctrlStatus.Volume.Level,
|
||||||
|
Muted: *ctrlStatus.Volume.Muted,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ctrlStatus.Applications) > 0 {
|
||||||
|
status.CurrentApp.ID = *ctrlStatus.Applications[0].AppID
|
||||||
|
status.CurrentApp.DisplayName = *ctrlStatus.Applications[0].DisplayName
|
||||||
|
status.CurrentApp.StatusText = *ctrlStatus.Applications[0].StatusText
|
||||||
|
}
|
||||||
|
|
||||||
|
return status, nil
|
||||||
|
}
|
63
pkg/module/cast/cast_test.go
Normal file
63
pkg/module/cast/cast_test.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package cast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cdr.dev/slog"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCastLoadURL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if os.Getenv("TEST_CAST_MODULE") != "yes" {
|
||||||
|
t.Skip("Test skipped. Set environment variable TEST_CAST_MODULE=yes to run.")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
devices, err := findDevices(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, len(devices); e != g {
|
||||||
|
t.Fatalf("len(devices): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
dev := devices[0]
|
||||||
|
|
||||||
|
ctx, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel2()
|
||||||
|
|
||||||
|
if err := loadURL(ctx, dev.UUID, "https://go.dev"); err != nil {
|
||||||
|
t.Error(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel3()
|
||||||
|
|
||||||
|
status, err := getStatus(ctx, dev.UUID)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
spew.Dump(status)
|
||||||
|
|
||||||
|
ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel4()
|
||||||
|
|
||||||
|
if err := stopCast(ctx, dev.UUID); err != nil {
|
||||||
|
t.Error(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
5
pkg/module/cast/error.go
Normal file
5
pkg/module/cast/error.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package cast
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrDeviceNotFound = errors.New("device not found")
|
263
pkg/module/cast/module.go
Normal file
263
pkg/module/cast/module.go
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
package cast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return "cast"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Export(export *goja.Object) {
|
||||||
|
if err := export.Set("refreshDevices", m.refreshDevices); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'refreshDevices' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("getDevices", m.getDevices); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'getDevices' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("loadUrl", m.loadUrl); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'loadUrl' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("stopCast", m.stopCast); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'stopCast' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("getStatus", m.getStatus); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'getStatus' function"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
rawTimeout := call.Argument(0).String()
|
||||||
|
|
||||||
|
timeout, err := m.parseTimeout(rawTimeout)
|
||||||
|
if err != nil {
|
||||||
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "error refreshing casting devices list", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
promise.Reject(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
m.mutex.devices.Lock()
|
||||||
|
m.devices = devices
|
||||||
|
m.mutex.devices.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
devicesCopy := m.getDevicesCopy(devices)
|
||||||
|
promise.Resolve(devicesCopy)
|
||||||
|
}()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
devices := m.getDevicesCopy(m.devices)
|
||||||
|
|
||||||
|
return rt.ToValue(devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
if len(call.Arguments) < 2 {
|
||||||
|
panic(rt.ToValue(errors.WithStack(module.ErrUnexpectedArgumentsNumber)))
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceUUID := call.Argument(0).String()
|
||||||
|
url := call.Argument(1).String()
|
||||||
|
|
||||||
|
rawTimeout := call.Argument(2).String()
|
||||||
|
|
||||||
|
timeout, err := m.parseTimeout(rawTimeout)
|
||||||
|
if err != nil {
|
||||||
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
promise := m.server.NewPromise()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
m.mutex.loadURL.Lock()
|
||||||
|
defer m.mutex.loadURL.Unlock()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := loadURL(ctx, deviceUUID, url)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "error while casting url", logger.E(err))
|
||||||
|
|
||||||
|
promise.Reject(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.Resolve(nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return m.server.ToValue(promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) stopCast(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
if len(call.Arguments) < 1 {
|
||||||
|
panic(rt.ToValue(errors.WithStack(module.ErrUnexpectedArgumentsNumber)))
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceUUID := call.Argument(0).String()
|
||||||
|
rawTimeout := call.Argument(1).String()
|
||||||
|
|
||||||
|
timeout, err := m.parseTimeout(rawTimeout)
|
||||||
|
if err != nil {
|
||||||
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
promise := m.server.NewPromise()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
m.mutex.quitApp.Lock()
|
||||||
|
defer m.mutex.quitApp.Unlock()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := stopCast(ctx, deviceUUID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "error while quitting casting device app", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
promise.Reject(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.Resolve(nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return m.server.ToValue(promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
if len(call.Arguments) < 1 {
|
||||||
|
panic(rt.ToValue(errors.WithStack(module.ErrUnexpectedArgumentsNumber)))
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceUUID := call.Argument(0).String()
|
||||||
|
rawTimeout := call.Argument(1).String()
|
||||||
|
|
||||||
|
timeout, err := m.parseTimeout(rawTimeout)
|
||||||
|
if err != nil {
|
||||||
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
promise := m.server.NewPromise()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
m.mutex.getStatus.Lock()
|
||||||
|
defer m.mutex.getStatus.Unlock()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
status, err := getStatus(ctx, deviceUUID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "error while getting casting device status", logger.E(err))
|
||||||
|
|
||||||
|
promise.Reject(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.Resolve(status)
|
||||||
|
}()
|
||||||
|
|
||||||
|
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
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if rawTimeout == "undefined" {
|
||||||
|
timeout = defaultTimeout
|
||||||
|
} else {
|
||||||
|
timeout, err = time.ParseDuration(rawTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return defaultTimeout, errors.Wrapf(err, "invalid duration format '%s'", rawTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeout, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CastModuleFactory() app.ServerModuleFactory {
|
||||||
|
return func(server *app.Server) app.ServerModule {
|
||||||
|
return &Module{
|
||||||
|
server: server,
|
||||||
|
devices: make([]*Device, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
95
pkg/module/cast/module_test.go
Normal file
95
pkg/module/cast/module_test.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package cast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cdr.dev/slog"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCastModule(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if os.Getenv("TEST_CAST_MODULE") != "yes" {
|
||||||
|
t.Skip("Test skipped. Set environment variable TEST_CAST_MODULE=yes to run.")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
|
||||||
|
server := app.NewServer(
|
||||||
|
module.ConsoleModuleFactory(),
|
||||||
|
CastModuleFactory(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile("testdata/cast.js")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Load("testdata/cast.js", string(data)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCastModuleRefreshDevices(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if os.Getenv("TEST_CAST_MODULE") != "yes" {
|
||||||
|
t.Skip("Test skipped. Set environment variable TEST_CAST_MODULE=yes to run.")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
|
||||||
|
server := app.NewServer(
|
||||||
|
module.ConsoleModuleFactory(),
|
||||||
|
CastModuleFactory(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile("testdata/refresh_devices.js")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Load("testdata/refresh_devices.js", string(data)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
result, err := server.ExecFuncByName("refreshDevices")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
promise, ok := server.IsPromise(result)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected promise")
|
||||||
|
}
|
||||||
|
|
||||||
|
value := server.WaitForPromise(promise)
|
||||||
|
|
||||||
|
spew.Dump(value.Export())
|
||||||
|
}
|
21
pkg/module/cast/testdata/cast.js
vendored
Normal file
21
pkg/module/cast/testdata/cast.js
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
cast.refreshDevices('5s')
|
||||||
|
.then(function(devices) {
|
||||||
|
console.log(devices)
|
||||||
|
|
||||||
|
if (devices === null) {
|
||||||
|
throw new Error("devices should not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devices.length === 0) {
|
||||||
|
throw new Error("devices.length should not be 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices
|
||||||
|
})
|
||||||
|
.then(function(devices) {
|
||||||
|
return cast.getStatus(devices[0].uuid)
|
||||||
|
})
|
||||||
|
.then(function(status) {
|
||||||
|
console.log(status)
|
||||||
|
})
|
6
pkg/module/cast/testdata/refresh_devices.js
vendored
Normal file
6
pkg/module/cast/testdata/refresh_devices.js
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
function refreshDevices() {
|
||||||
|
return cast.refreshDevices('5s')
|
||||||
|
.then(function(devices) {
|
||||||
|
return devices
|
||||||
|
})
|
||||||
|
}
|
@ -4,17 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContextKey string
|
type ContextKey string
|
||||||
|
|
||||||
const (
|
|
||||||
ContextKeySessionID ContextKey = "sessionId"
|
|
||||||
ContextKeyOriginRequest ContextKey = "originRequest"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ContextModule struct{}
|
type ContextModule struct{}
|
||||||
|
|
||||||
func (m *ContextModule) Name() string {
|
func (m *ContextModule) Name() string {
|
||||||
@ -26,8 +22,8 @@ func (m *ContextModule) new(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *ContextModule) with(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *ContextModule) with(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
rawValues := assertObject(call.Argument(1), rt)
|
rawValues := util.AssertObject(call.Argument(1), rt)
|
||||||
|
|
||||||
values := make(map[ContextKey]any)
|
values := make(map[ContextKey]any)
|
||||||
for k, v := range rawValues {
|
for k, v := range rawValues {
|
||||||
@ -40,8 +36,8 @@ func (m *ContextModule) with(call goja.FunctionCall, rt *goja.Runtime) goja.Valu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *ContextModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *ContextModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
rawKey := assertString(call.Argument(1), rt)
|
rawKey := util.AssertString(call.Argument(1), rt)
|
||||||
|
|
||||||
value := ctx.Value(ContextKey(rawKey))
|
value := ctx.Value(ContextKey(rawKey))
|
||||||
|
|
||||||
@ -60,14 +56,6 @@ func (m *ContextModule) Export(export *goja.Object) {
|
|||||||
if err := export.Set("with", m.with); err != nil {
|
if err := export.Set("with", m.with); err != nil {
|
||||||
panic(errors.Wrap(err, "could not set 'with' function"))
|
panic(errors.Wrap(err, "could not set 'with' function"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := export.Set("ORIGIN_REQUEST", string(ContextKeyOriginRequest)); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'ORIGIN_REQUEST' property"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := export.Set("SESSION_ID", string(ContextKeySessionID)); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'SESSION_ID' property"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ContextModuleFactory() app.ServerModuleFactory {
|
func ContextModuleFactory() app.ServerModuleFactory {
|
||||||
|
40
pkg/module/extension.go
Normal file
40
pkg/module/extension.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package module
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExtensionFunc func(*goja.Object)
|
||||||
|
|
||||||
|
type ExtendedModule struct {
|
||||||
|
module app.ServerModule
|
||||||
|
extensions []ExtensionFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export implements app.ServerModule.
|
||||||
|
func (m *ExtendedModule) Export(exports *goja.Object) {
|
||||||
|
m.module.Export(exports)
|
||||||
|
|
||||||
|
for _, ext := range m.extensions {
|
||||||
|
ext(exports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements app.ServerModule.
|
||||||
|
func (m *ExtendedModule) Name() string {
|
||||||
|
return m.module.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Extends(factory app.ServerModuleFactory, extensions ...ExtensionFunc) app.ServerModuleFactory {
|
||||||
|
return func(s *app.Server) app.ServerModule {
|
||||||
|
module := factory(s)
|
||||||
|
|
||||||
|
return &ExtendedModule{
|
||||||
|
module: module,
|
||||||
|
extensions: extensions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ app.ServerModule = &ExtendedModule{}
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
@ -12,7 +11,6 @@ import (
|
|||||||
|
|
||||||
type LifecycleModule struct {
|
type LifecycleModule struct {
|
||||||
server *app.Server
|
server *app.Server
|
||||||
bus bus.Bus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *LifecycleModule) Name() string {
|
func (m *LifecycleModule) Name() string {
|
||||||
@ -36,84 +34,12 @@ func (m *LifecycleModule) OnInit() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *LifecycleModule) handleMessages() {
|
func LifecycleModuleFactory() app.ServerModuleFactory {
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"subscribing to bus messages",
|
|
||||||
)
|
|
||||||
|
|
||||||
clientMessages, err := m.bus.Subscribe(ctx, MessageNamespaceClient)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"unsubscribing from bus messages",
|
|
||||||
)
|
|
||||||
|
|
||||||
m.bus.Unsubscribe(ctx, MessageNamespaceClient, clientMessages)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"waiting for next message",
|
|
||||||
)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"context done",
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
case msg := <-clientMessages:
|
|
||||||
clientMessage, ok := msg.(*ClientMessage)
|
|
||||||
if !ok {
|
|
||||||
logger.Error(
|
|
||||||
ctx,
|
|
||||||
"unexpected message type",
|
|
||||||
logger.F("message", msg),
|
|
||||||
)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"received client message",
|
|
||||||
logger.F("message", clientMessage),
|
|
||||||
)
|
|
||||||
|
|
||||||
if _, err := m.server.ExecFuncByName("onClientMessage", clientMessage.Context, clientMessage.Data); err != nil {
|
|
||||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error(
|
|
||||||
ctx,
|
|
||||||
"on client message error",
|
|
||||||
logger.E(err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LifecycleModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
|
||||||
return func(server *app.Server) app.ServerModule {
|
return func(server *app.Server) app.ServerModule {
|
||||||
module := &LifecycleModule{
|
module := &LifecycleModule{
|
||||||
server: server,
|
server: server,
|
||||||
bus: bus,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go module.handleMessages()
|
|
||||||
|
|
||||||
return module
|
return module
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
package module
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NetModule struct {
|
|
||||||
server *app.Server
|
|
||||||
bus bus.Bus
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NetModule) Name() string {
|
|
||||||
return "net"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NetModule) Export(export *goja.Object) {
|
|
||||||
if err := export.Set("broadcast", m.broadcast); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'broadcast' function"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := export.Set("send", m.send); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'send' function"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NetModule) broadcast(call goja.FunctionCall) goja.Value {
|
|
||||||
if len(call.Arguments) < 1 {
|
|
||||||
panic(m.server.ToValue("invalid number of argument"))
|
|
||||||
}
|
|
||||||
|
|
||||||
data := call.Argument(0).Export()
|
|
||||||
|
|
||||||
msg := NewServerMessage(nil, data)
|
|
||||||
if err := m.bus.Publish(context.Background(), msg); err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NetModule) send(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
||||||
if len(call.Arguments) < 2 {
|
|
||||||
panic(m.server.ToValue("invalid number of argument"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var ctx context.Context
|
|
||||||
|
|
||||||
firstArg := call.Argument(0)
|
|
||||||
|
|
||||||
sessionID, ok := firstArg.Export().(string)
|
|
||||||
if ok {
|
|
||||||
ctx = WithContext(context.Background(), map[ContextKey]any{
|
|
||||||
ContextKeySessionID: sessionID,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
ctx = assertContext(firstArg, rt)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := call.Argument(1).Export()
|
|
||||||
|
|
||||||
msg := NewServerMessage(ctx, data)
|
|
||||||
if err := m.bus.Publish(ctx, msg); err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NetModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
|
||||||
return func(server *app.Server) app.ServerModule {
|
|
||||||
return &NetModule{
|
|
||||||
server: server,
|
|
||||||
bus: bus,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
158
pkg/module/net/module.go
Normal file
158
pkg/module/net/module.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module struct {
|
||||||
|
server *app.Server
|
||||||
|
bus bus.Bus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Name() string {
|
||||||
|
return "net"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Export(export *goja.Object) {
|
||||||
|
if err := export.Set("broadcast", m.broadcast); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'broadcast' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("send", m.send); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'send' function"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) broadcast(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
if len(call.Arguments) < 1 {
|
||||||
|
panic(rt.ToValue(errors.New("invalid number of argument")))
|
||||||
|
}
|
||||||
|
|
||||||
|
data := call.Argument(0).Export()
|
||||||
|
|
||||||
|
msg := module.NewServerMessage(nil, data)
|
||||||
|
if err := m.bus.Publish(context.Background(), msg); err != nil {
|
||||||
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) send(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
if len(call.Arguments) < 2 {
|
||||||
|
panic(rt.ToValue(errors.New("invalid number of argument")))
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
|
||||||
|
firstArg := call.Argument(0)
|
||||||
|
|
||||||
|
sessionID, ok := firstArg.Export().(string)
|
||||||
|
if ok {
|
||||||
|
ctx = module.WithContext(context.Background(), map[module.ContextKey]any{
|
||||||
|
edgeHTTP.ContextKeySessionID: sessionID,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ctx = util.AssertContext(firstArg, rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := call.Argument(1).Export()
|
||||||
|
|
||||||
|
msg := module.NewServerMessage(ctx, data)
|
||||||
|
if err := m.bus.Publish(ctx, msg); err != nil {
|
||||||
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) handleClientMessages() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"subscribing to bus messages",
|
||||||
|
)
|
||||||
|
|
||||||
|
clientMessages, err := m.bus.Subscribe(ctx, module.MessageNamespaceClient)
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"unsubscribing from bus messages",
|
||||||
|
)
|
||||||
|
|
||||||
|
m.bus.Unsubscribe(ctx, module.MessageNamespaceClient, clientMessages)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"waiting for next message",
|
||||||
|
)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"context done",
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
case msg := <-clientMessages:
|
||||||
|
clientMessage, ok := msg.(*module.ClientMessage)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(
|
||||||
|
ctx,
|
||||||
|
"unexpected message type",
|
||||||
|
logger.F("message", msg),
|
||||||
|
)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"received client message",
|
||||||
|
logger.F("message", clientMessage),
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, err := m.server.ExecFuncByName("onClientMessage", clientMessage.Context, clientMessage.Data); err != nil {
|
||||||
|
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(
|
||||||
|
ctx,
|
||||||
|
"on client message error",
|
||||||
|
logger.E(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
||||||
|
return func(server *app.Server) app.ServerModule {
|
||||||
|
module := &Module{
|
||||||
|
server: server,
|
||||||
|
bus: bus,
|
||||||
|
}
|
||||||
|
|
||||||
|
go module.handleClientMessages()
|
||||||
|
|
||||||
|
return module
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
@ -51,7 +52,7 @@ func (m *RPCModule) Export(export *goja.Object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
fnName := assertString(call.Argument(0), rt)
|
fnName := util.AssertString(call.Argument(0), rt)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
callable goja.Callable
|
callable goja.Callable
|
||||||
@ -78,7 +79,7 @@ func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Valu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *RPCModule) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *RPCModule) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
fnName := assertString(call.Argument(0), rt)
|
fnName := util.AssertString(call.Argument(0), rt)
|
||||||
|
|
||||||
m.callbacks.Delete(fnName)
|
m.callbacks.Delete(fnName)
|
||||||
|
|
||||||
@ -160,8 +161,14 @@ func (m *RPCModule) handleMessages() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := m.server.Exec(callable, ctx, req.Params)
|
result, err := m.server.Exec(callable, clientMessage.Context, req.Params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "rpc call error",
|
||||||
|
logger.E(errors.WithStack(err)),
|
||||||
|
logger.F("request", req),
|
||||||
|
)
|
||||||
|
|
||||||
if err := m.sendErrorResponse(clientMessage.Context, req, err); err != nil {
|
if err := m.sendErrorResponse(clientMessage.Context, req, err); err != nil {
|
||||||
logger.Error(
|
logger.Error(
|
||||||
ctx, "could not send error response",
|
ctx, "could not send error response",
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
@ -47,20 +48,20 @@ func (m *StoreModule) Export(export *goja.Object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *StoreModule) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
collection := m.assertCollection(call.Argument(1), rt)
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
document := m.assertDocument(call.Argument(2), rt)
|
document := m.assertDocument(call.Argument(2), rt)
|
||||||
|
|
||||||
document, err := m.store.Upsert(ctx, collection, document)
|
document, err := m.store.Upsert(ctx, collection, document)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.Wrapf(err, "error while upserting document in collection '%s'", collection))
|
panic(rt.ToValue(errors.Wrapf(err, "error while upserting document in collection '%s'", collection)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return rt.ToValue(map[string]interface{}(document))
|
return rt.ToValue(map[string]interface{}(document))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *StoreModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
collection := m.assertCollection(call.Argument(1), rt)
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
documentID := m.assertDocumentID(call.Argument(2), rt)
|
documentID := m.assertDocumentID(call.Argument(2), rt)
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ func (m *StoreModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(errors.Wrapf(err, "error while getting document '%s' in collection '%s'", documentID, collection))
|
panic(rt.ToValue(errors.Wrapf(err, "error while getting document '%s' in collection '%s'", documentID, collection)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return rt.ToValue(map[string]interface{}(document))
|
return rt.ToValue(map[string]interface{}(document))
|
||||||
@ -84,34 +85,36 @@ type queryOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
collection := m.assertCollection(call.Argument(1), rt)
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
filter := m.assertFilter(call.Argument(2), rt)
|
filter := m.assertFilter(call.Argument(2), rt)
|
||||||
queryOptions := m.assertQueryOptions(call.Argument(3), rt)
|
queryOptions := m.assertQueryOptions(call.Argument(3), rt)
|
||||||
|
|
||||||
queryOptionsFuncs := make([]storage.QueryOptionFunc, 0)
|
queryOptionsFuncs := make([]storage.QueryOptionFunc, 0)
|
||||||
|
|
||||||
if queryOptions.Limit != nil {
|
if queryOptions != nil {
|
||||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithLimit(*queryOptions.Limit))
|
if queryOptions.Limit != nil {
|
||||||
}
|
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithLimit(*queryOptions.Limit))
|
||||||
|
}
|
||||||
|
|
||||||
if queryOptions.OrderBy != nil {
|
if queryOptions.OrderBy != nil {
|
||||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderBy(*queryOptions.OrderBy))
|
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderBy(*queryOptions.OrderBy))
|
||||||
}
|
}
|
||||||
|
|
||||||
if queryOptions.Offset != nil {
|
if queryOptions.Offset != nil {
|
||||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOffset(*queryOptions.Limit))
|
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOffset(*queryOptions.Limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
if queryOptions.OrderDirection != nil {
|
if queryOptions.OrderDirection != nil {
|
||||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderDirection(
|
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderDirection(
|
||||||
storage.OrderDirection(*queryOptions.OrderDirection),
|
storage.OrderDirection(*queryOptions.OrderDirection),
|
||||||
))
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
documents, err := m.store.Query(ctx, collection, filter, queryOptionsFuncs...)
|
documents, err := m.store.Query(ctx, collection, filter, queryOptionsFuncs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.Wrapf(err, "error while querying documents in collection '%s'", collection))
|
panic(rt.ToValue(errors.Wrapf(err, "error while querying documents in collection '%s'", collection)))
|
||||||
}
|
}
|
||||||
|
|
||||||
rawDocuments := make([]map[string]interface{}, len(documents))
|
rawDocuments := make([]map[string]interface{}, len(documents))
|
||||||
@ -123,12 +126,12 @@ func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) delete(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *StoreModule) delete(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
collection := m.assertCollection(call.Argument(1), rt)
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
documentID := m.assertDocumentID(call.Argument(2), rt)
|
documentID := m.assertDocumentID(call.Argument(2), rt)
|
||||||
|
|
||||||
if err := m.store.Delete(ctx, collection, documentID); err != nil {
|
if err := m.store.Delete(ctx, collection, documentID); err != nil {
|
||||||
panic(errors.Wrapf(err, "error while deleting document '%s' in collection '%s'", documentID, collection))
|
panic(rt.ToValue(errors.Wrapf(err, "error while deleting document '%s' in collection '%s'", documentID, collection)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -144,6 +147,10 @@ func (m *StoreModule) assertCollection(value goja.Value, rt *goja.Runtime) strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) assertFilter(value goja.Value, rt *goja.Runtime) *filter.Filter {
|
func (m *StoreModule) assertFilter(value goja.Value, rt *goja.Runtime) *filter.Filter {
|
||||||
|
if value.Export() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
rawFilter, ok := value.Export().(map[string]interface{})
|
rawFilter, ok := value.Export().(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(rt.NewTypeError(fmt.Sprintf("filter must be an object, got '%T'", value.Export())))
|
panic(rt.NewTypeError(fmt.Sprintf("filter must be an object, got '%T'", value.Export())))
|
||||||
@ -151,7 +158,7 @@ func (m *StoreModule) assertFilter(value goja.Value, rt *goja.Runtime) *filter.F
|
|||||||
|
|
||||||
filter, err := filter.NewFrom(rawFilter)
|
filter, err := filter.NewFrom(rawFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.Wrap(err, "could not convert object to filter"))
|
panic(rt.ToValue(errors.Wrap(err, "could not convert object to filter")))
|
||||||
}
|
}
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
@ -172,6 +179,10 @@ func (m *StoreModule) assertDocumentID(value goja.Value, rt *goja.Runtime) stora
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) assertQueryOptions(value goja.Value, rt *goja.Runtime) *queryOptions {
|
func (m *StoreModule) assertQueryOptions(value goja.Value, rt *goja.Runtime) *queryOptions {
|
||||||
|
if value.Export() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
rawQueryOptions, ok := value.Export().(map[string]interface{})
|
rawQueryOptions, ok := value.Export().(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(rt.NewTypeError(fmt.Sprintf("query options must be an object, got '%T'", value.Export())))
|
panic(rt.NewTypeError(fmt.Sprintf("query options must be an object, got '%T'", value.Export())))
|
||||||
@ -180,7 +191,7 @@ func (m *StoreModule) assertQueryOptions(value goja.Value, rt *goja.Runtime) *qu
|
|||||||
queryOptions := &queryOptions{}
|
queryOptions := &queryOptions{}
|
||||||
|
|
||||||
if err := mapstructure.Decode(rawQueryOptions, queryOptions); err != nil {
|
if err := mapstructure.Decode(rawQueryOptions, queryOptions); err != nil {
|
||||||
panic(errors.Wrap(err, "could not convert object to query options"))
|
panic(rt.ToValue(errors.Wrap(err, "could not convert object to query options")))
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryOptions
|
return queryOptions
|
||||||
|
216
pkg/module/store/module.go
Normal file
216
pkg/module/store/module.go
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module struct {
|
||||||
|
server *app.Server
|
||||||
|
store storage.DocumentStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Name() string {
|
||||||
|
return "store"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Export(export *goja.Object) {
|
||||||
|
if err := export.Set("upsert", m.upsert); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'upsert' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("get", m.get); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'get' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("query", m.query); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'query' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("delete", m.delete); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'delete' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("DIRECTION_ASC", storage.OrderDirectionAsc); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'DIRECTION_ASC' property"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("DIRECTION_DESC", storage.OrderDirectionDesc); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'DIRECTION_DESC' property"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
|
document := m.assertDocument(call.Argument(2), rt)
|
||||||
|
|
||||||
|
document, err := m.store.Upsert(ctx, collection, document)
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.Wrapf(err, "error while upserting document in collection '%s'", collection))
|
||||||
|
}
|
||||||
|
|
||||||
|
return rt.ToValue(map[string]interface{}(document))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
|
documentID := m.assertDocumentID(call.Argument(2), rt)
|
||||||
|
|
||||||
|
document, err := m.store.Get(ctx, collection, documentID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrDocumentNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(errors.Wrapf(err, "error while getting document '%s' in collection '%s'", documentID, collection))
|
||||||
|
}
|
||||||
|
|
||||||
|
return rt.ToValue(map[string]interface{}(document))
|
||||||
|
}
|
||||||
|
|
||||||
|
type queryOptions struct {
|
||||||
|
Limit *int `mapstructure:"limit"`
|
||||||
|
Offset *int `mapstructure:"offset"`
|
||||||
|
OrderBy *string `mapstructure:"orderBy"`
|
||||||
|
OrderDirection *string `mapstructure:"orderDirection"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
|
filter := m.assertFilter(call.Argument(2), rt)
|
||||||
|
queryOptions := m.assertQueryOptions(call.Argument(3), rt)
|
||||||
|
|
||||||
|
queryOptionsFuncs := make([]storage.QueryOptionFunc, 0)
|
||||||
|
|
||||||
|
if queryOptions != nil {
|
||||||
|
if queryOptions.Limit != nil {
|
||||||
|
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithLimit(*queryOptions.Limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
if queryOptions.OrderBy != nil {
|
||||||
|
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderBy(*queryOptions.OrderBy))
|
||||||
|
}
|
||||||
|
|
||||||
|
if queryOptions.Offset != nil {
|
||||||
|
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOffset(*queryOptions.Limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
if queryOptions.OrderDirection != nil {
|
||||||
|
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderDirection(
|
||||||
|
storage.OrderDirection(*queryOptions.OrderDirection),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
documents, err := m.store.Query(ctx, collection, filter, queryOptionsFuncs...)
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.Wrapf(err, "error while querying documents in collection '%s'", collection))
|
||||||
|
}
|
||||||
|
|
||||||
|
rawDocuments := make([]map[string]interface{}, len(documents))
|
||||||
|
for idx, doc := range documents {
|
||||||
|
rawDocuments[idx] = map[string]interface{}(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rt.ToValue(rawDocuments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) delete(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
|
documentID := m.assertDocumentID(call.Argument(2), rt)
|
||||||
|
|
||||||
|
if err := m.store.Delete(ctx, collection, documentID); err != nil {
|
||||||
|
panic(errors.Wrapf(err, "error while deleting document '%s' in collection '%s'", documentID, collection))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) assertCollection(value goja.Value, rt *goja.Runtime) string {
|
||||||
|
collection, ok := value.Export().(string)
|
||||||
|
if !ok {
|
||||||
|
panic(rt.NewTypeError(fmt.Sprintf("collection must be a string, got '%T'", value.Export())))
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) assertFilter(value goja.Value, rt *goja.Runtime) *filter.Filter {
|
||||||
|
if value.Export() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawFilter, ok := value.Export().(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
panic(rt.NewTypeError(fmt.Sprintf("filter must be an object, got '%T'", value.Export())))
|
||||||
|
}
|
||||||
|
|
||||||
|
filter, err := filter.NewFrom(rawFilter)
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not convert object to filter"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) assertDocumentID(value goja.Value, rt *goja.Runtime) storage.DocumentID {
|
||||||
|
documentID, ok := value.Export().(storage.DocumentID)
|
||||||
|
if !ok {
|
||||||
|
rawDocumentID, ok := value.Export().(string)
|
||||||
|
if !ok {
|
||||||
|
panic(rt.NewTypeError(fmt.Sprintf("document id must be a documentid or a string, got '%T'", value.Export())))
|
||||||
|
}
|
||||||
|
|
||||||
|
documentID = storage.DocumentID(rawDocumentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return documentID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) assertQueryOptions(value goja.Value, rt *goja.Runtime) *queryOptions {
|
||||||
|
if value.Export() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawQueryOptions, ok := value.Export().(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
panic(rt.NewTypeError(fmt.Sprintf("query options must be an object, got '%T'", value.Export())))
|
||||||
|
}
|
||||||
|
|
||||||
|
queryOptions := &queryOptions{}
|
||||||
|
|
||||||
|
if err := mapstructure.Decode(rawQueryOptions, queryOptions); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not convert object to query options"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) assertDocument(value goja.Value, rt *goja.Runtime) storage.Document {
|
||||||
|
document, ok := value.Export().(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
panic(rt.NewTypeError("document must be an object"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return document
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModuleFactory(store storage.DocumentStore) app.ServerModuleFactory {
|
||||||
|
return func(server *app.Server) app.ServerModule {
|
||||||
|
return &Module{
|
||||||
|
server: server,
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
package module
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
@ -15,9 +16,9 @@ func TestStoreModule(t *testing.T) {
|
|||||||
|
|
||||||
store := sqlite.NewDocumentStore(":memory:")
|
store := sqlite.NewDocumentStore(":memory:")
|
||||||
server := app.NewServer(
|
server := app.NewServer(
|
||||||
ContextModuleFactory(),
|
module.ContextModuleFactory(),
|
||||||
ConsoleModuleFactory(),
|
module.ConsoleModuleFactory(),
|
||||||
StoreModuleFactory(store),
|
ModuleFactory(store),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("testdata/store.js")
|
data, err := ioutil.ReadFile("testdata/store.js")
|
@ -1,59 +0,0 @@
|
|||||||
package module
|
|
||||||
|
|
||||||
// import (
|
|
||||||
// "context"
|
|
||||||
|
|
||||||
// "github.com/dop251/goja"
|
|
||||||
// "github.com/pkg/errors"
|
|
||||||
// "forge.cadoles.com/arcad/edge/pkg/app"
|
|
||||||
// "forge.cadoles.com/arcad/edge/pkg/repository"
|
|
||||||
// "gitlab.com/wpetit/goweb/logger"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// type UserModule struct {
|
|
||||||
// appID app.ID
|
|
||||||
// repo repository.UserRepository
|
|
||||||
// backend *app.Server
|
|
||||||
// ctx context.Context
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *UserModule) Name() string {
|
|
||||||
// return "user"
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *UserModule) Export(export *goja.Object) {
|
|
||||||
// if err := export.Set("getUserById", m.getUserByID); err != nil {
|
|
||||||
// panic(errors.Wrap(err, "could not set 'getUserById' function"))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *UserModule) getUserByID(call goja.FunctionCall) goja.Value {
|
|
||||||
// if len(call.Arguments) != 1 {
|
|
||||||
// panic(m.backend.ToValue("invalid number of arguments"))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// userID := repository.UserID(call.Arguments[0].String())
|
|
||||||
|
|
||||||
// user, err := m.repo.Get(userID)
|
|
||||||
// if err != nil {
|
|
||||||
// err = errors.Wrapf(err, "could not find user '%s'", userID)
|
|
||||||
// logger.Error(m.ctx, "could not find user", logger.E(err), logger.F("userID", userID))
|
|
||||||
// panic(m.backend.ToValue(err))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return m.backend.ToValue(user)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func UserModuleFactory(repo repository.UserRepository) app.ServerModuleFactory {
|
|
||||||
// return func(appID app.ID, backend *app.Server) app.ServerModule {
|
|
||||||
// return &UserModule{
|
|
||||||
// appID: appID,
|
|
||||||
// repo: repo,
|
|
||||||
// backend: backend,
|
|
||||||
// ctx: logger.With(
|
|
||||||
// context.Background(),
|
|
||||||
// logger.F("appID", appID),
|
|
||||||
// ),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
@ -1,70 +0,0 @@
|
|||||||
package module
|
|
||||||
|
|
||||||
// import (
|
|
||||||
// "errors"
|
|
||||||
// "io/ioutil"
|
|
||||||
// "testing"
|
|
||||||
|
|
||||||
// "gitlab.com/arcadbox/arcad/internal/app"
|
|
||||||
// "gitlab.com/arcadbox/arcad/internal/repository"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// func TestUserModuleGetUserByID(t *testing.T) {
|
|
||||||
// repo := &fakeUserRepository{}
|
|
||||||
|
|
||||||
// appID := app.ID("test")
|
|
||||||
// backend := app.NewServer(appID,
|
|
||||||
// ConsoleModuleFactory(),
|
|
||||||
// UserModuleFactory(repo),
|
|
||||||
// )
|
|
||||||
|
|
||||||
// data, err := ioutil.ReadFile("testdata/user_getbyid.js")
|
|
||||||
// if err != nil {
|
|
||||||
// t.Fatal(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if err := backend.Load(string(data)); err != nil {
|
|
||||||
// t.Fatal(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// backend.Start()
|
|
||||||
// defer backend.Stop()
|
|
||||||
|
|
||||||
// if err := backend.OnInit(); err != nil {
|
|
||||||
// t.Error(err)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// type fakeUserRepository struct{}
|
|
||||||
|
|
||||||
// func (r *fakeUserRepository) Create() (*repository.User, error) {
|
|
||||||
// return nil, errors.New("not implemented")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (r *fakeUserRepository) Save(user *repository.User) error {
|
|
||||||
// return errors.New("not implemented")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (r *fakeUserRepository) Get(userID repository.UserID) (*repository.User, error) {
|
|
||||||
// if userID == "0" {
|
|
||||||
// return &repository.User{}, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return nil, errors.New("not implemented")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (r *fakeUserRepository) Delete(userID repository.UserID) error {
|
|
||||||
// return errors.New("not implemented")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (r *fakeUserRepository) Touch(userID repository.UserID, rawUserAgent string) error {
|
|
||||||
// return errors.New("not implemented")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (r *fakeUserRepository) List() ([]*repository.User, error) {
|
|
||||||
// return nil, errors.New("not implemented")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (r *fakeUserRepository) ListByID(userIDs ...repository.UserID) ([]*repository.User, error) {
|
|
||||||
// return nil, errors.New("not implemented")
|
|
||||||
// }
|
|
28
pkg/module/util/assert.go
Normal file
28
pkg/module/util/assert.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AssertType[T any](v goja.Value, rt *goja.Runtime) T {
|
||||||
|
if c, ok := v.Export().(T); ok {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(rt.ToValue(errors.Errorf("expected value to be a '%T', got '%T'", *new(T), v.Export())))
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertContext(v goja.Value, r *goja.Runtime) context.Context {
|
||||||
|
return AssertType[context.Context](v, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertObject(v goja.Value, r *goja.Runtime) map[string]any {
|
||||||
|
return AssertType[map[string]any](v, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertString(v goja.Value, r *goja.Runtime) string {
|
||||||
|
return AssertType[string](v, r)
|
||||||
|
}
|
@ -90,8 +90,6 @@ export class Client extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleRPCResponse(evt) {
|
_handleRPCResponse(evt) {
|
||||||
console.log(evt);
|
|
||||||
|
|
||||||
const { jsonrpc, id, error, result } = evt.detail;
|
const { jsonrpc, id, error, result } = evt.detail;
|
||||||
|
|
||||||
if (jsonrpc !== '2.0' || id === undefined) return;
|
if (jsonrpc !== '2.0' || id === undefined) return;
|
||||||
|
@ -31,12 +31,17 @@ func (d Document) ID() (DocumentID, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
id, ok := rawID.(string)
|
strID, ok := rawID.(string)
|
||||||
if ok {
|
if ok {
|
||||||
return "", false
|
return DocumentID(strID), true
|
||||||
}
|
}
|
||||||
|
|
||||||
return DocumentID(id), true
|
docID, ok := rawID.(DocumentID)
|
||||||
|
if ok {
|
||||||
|
return docID, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Document) CreatedAt() (time.Time, bool) {
|
func (d Document) CreatedAt() (time.Time, bool) {
|
||||||
@ -54,7 +59,7 @@ func (d Document) timeAttr(attr string) (time.Time, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t, ok := rawTime.(time.Time)
|
t, ok := rawTime.(time.Time)
|
||||||
if ok {
|
if !ok {
|
||||||
return time.Time{}, false
|
return time.Time{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
pkg/storage/filter/sql/operator.go
Normal file
15
pkg/storage/filter/sql/operator.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
const (
|
||||||
|
OpIn = "IN"
|
||||||
|
OpLesserThan = "<"
|
||||||
|
OpLesserThanEqual = "<="
|
||||||
|
OpEqual = "="
|
||||||
|
OpNotEqual = "!="
|
||||||
|
OpSuperiorThan = ">"
|
||||||
|
OpSuperiorThanEqual = ">="
|
||||||
|
OpAnd = "AND"
|
||||||
|
OpOr = "OR"
|
||||||
|
OpLike = "LIKE"
|
||||||
|
OpNot = "NOT"
|
||||||
|
)
|
@ -71,6 +71,12 @@ func WithDefaultTransform() OptionFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithTransform(transform TransformFunc) OptionFunc {
|
||||||
|
return func(opt *Option) {
|
||||||
|
opt.Transform = transform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithNoOpValueTransform() OptionFunc {
|
func WithNoOpValueTransform() OptionFunc {
|
||||||
return WithValueTransform(func(value interface{}) interface{} {
|
return WithValueTransform(func(value interface{}) interface{} {
|
||||||
return value
|
return value
|
||||||
|
@ -60,7 +60,7 @@ func transformAndOperator(op filter.Operator, option *Option) (string, []interfa
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenAnd, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenAnd, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return aggregatorToSQL("AND", option, andOp.Children()...)
|
return aggregatorToSQL(OpAnd, option, andOp.Children()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformOrOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformOrOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -69,7 +69,7 @@ func transformOrOperator(op filter.Operator, option *Option) (string, []interfac
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenOr, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenOr, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return aggregatorToSQL("OR", option, orOp.Children()...)
|
return aggregatorToSQL(OpOr, option, orOp.Children()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformEqOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformEqOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -78,7 +78,7 @@ func transformEqOperator(op filter.Operator, option *Option) (string, []interfac
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenEq, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenEq, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldsToSQL("=", false, eqOp.Fields(), option)
|
return fieldsToSQL(OpEqual, false, eqOp.Fields(), option)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformNeqOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformNeqOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -87,7 +87,7 @@ func transformNeqOperator(op filter.Operator, option *Option) (string, []interfa
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenNeq, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenNeq, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldsToSQL("!=", false, eqOp.Fields(), option)
|
return fieldsToSQL(OpNotEqual, false, eqOp.Fields(), option)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformGtOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformGtOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -96,7 +96,7 @@ func transformGtOperator(op filter.Operator, option *Option) (string, []interfac
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenGt, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenGt, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldsToSQL(">", false, gtOp.Fields(), option)
|
return fieldsToSQL(OpSuperiorThan, false, gtOp.Fields(), option)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformGteOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformGteOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -105,7 +105,7 @@ func transformGteOperator(op filter.Operator, option *Option) (string, []interfa
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenGte, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenGte, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldsToSQL(">=", false, gteOp.Fields(), option)
|
return fieldsToSQL(OpSuperiorThanEqual, false, gteOp.Fields(), option)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformLtOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformLtOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -114,7 +114,7 @@ func transformLtOperator(op filter.Operator, option *Option) (string, []interfac
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLt, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLt, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldsToSQL("<", false, ltOp.Fields(), option)
|
return fieldsToSQL(OpLesserThan, false, ltOp.Fields(), option)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformLteOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformLteOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -123,7 +123,7 @@ func transformLteOperator(op filter.Operator, option *Option) (string, []interfa
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLte, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLte, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldsToSQL("<=", false, lteOp.Fields(), option)
|
return fieldsToSQL(OpLesserThanEqual, false, lteOp.Fields(), option)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformInOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformInOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -132,7 +132,7 @@ func transformInOperator(op filter.Operator, option *Option) (string, []interfac
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenIn, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenIn, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldsToSQL("IN", true, inOp.Fields(), option)
|
return fieldsToSQL(OpIn, true, inOp.Fields(), option)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformLikeOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformLikeOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -141,7 +141,7 @@ func transformLikeOperator(op filter.Operator, option *Option) (string, []interf
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLike, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLike, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldsToSQL("LIKE", false, likeOp.Fields(), option)
|
return fieldsToSQL(OpLike, false, likeOp.Fields(), option)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformNotOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
func transformNotOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
|
||||||
@ -150,10 +150,10 @@ func transformNotOperator(op filter.Operator, option *Option) (string, []interfa
|
|||||||
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenNot, op.Token())
|
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenNot, op.Token())
|
||||||
}
|
}
|
||||||
|
|
||||||
sql, args, err := aggregatorToSQL("AND", option, notOp.Children()...)
|
sql, args, err := aggregatorToSQL(OpAnd, option, notOp.Children()...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, errors.WithStack(err)
|
return "", nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "NOT " + sql, args, nil
|
return OpNot + " " + sql, args, nil
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ func DefaultTransform(operator string, invert bool, key string, value interface{
|
|||||||
return "", nil, errors.WithStack(err)
|
return "", nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := sb.WriteString(key); err != nil {
|
if _, err := sb.WriteString(option.KeyTransform(key)); err != nil {
|
||||||
return "", nil, errors.WithStack(err)
|
return "", nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -7,35 +7,35 @@ const (
|
|||||||
OrderDirectionDesc OrderDirection = "DESC"
|
OrderDirectionDesc OrderDirection = "DESC"
|
||||||
)
|
)
|
||||||
|
|
||||||
type QueryOption struct {
|
type QueryOptions struct {
|
||||||
Limit *int
|
Limit *int
|
||||||
Offset *int
|
Offset *int
|
||||||
OrderBy *string
|
OrderBy *string
|
||||||
OrderDirection *OrderDirection
|
OrderDirection *OrderDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryOptionFunc func(o *QueryOption)
|
type QueryOptionFunc func(o *QueryOptions)
|
||||||
|
|
||||||
func WithLimit(limit int) QueryOptionFunc {
|
func WithLimit(limit int) QueryOptionFunc {
|
||||||
return func(o *QueryOption) {
|
return func(o *QueryOptions) {
|
||||||
o.Limit = &limit
|
o.Limit = &limit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithOffset(offset int) QueryOptionFunc {
|
func WithOffset(offset int) QueryOptionFunc {
|
||||||
return func(o *QueryOption) {
|
return func(o *QueryOptions) {
|
||||||
o.Offset = &offset
|
o.Offset = &offset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithOrderBy(orderBy string) QueryOptionFunc {
|
func WithOrderBy(orderBy string) QueryOptionFunc {
|
||||||
return func(o *QueryOption) {
|
return func(o *QueryOptions) {
|
||||||
o.OrderBy = &orderBy
|
o.OrderBy = &orderBy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithOrderDirection(direction OrderDirection) QueryOptionFunc {
|
func WithOrderDirection(direction OrderDirection) QueryOptionFunc {
|
||||||
return func(o *QueryOption) {
|
return func(o *QueryOptions) {
|
||||||
o.OrderDirection = &direction
|
o.OrderDirection = &direction
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -90,18 +91,31 @@ func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.D
|
|||||||
|
|
||||||
// Query implements storage.DocumentStore
|
// Query implements storage.DocumentStore
|
||||||
func (s *DocumentStore) Query(ctx context.Context, collection string, filter *filter.Filter, funcs ...storage.QueryOptionFunc) ([]storage.Document, error) {
|
func (s *DocumentStore) Query(ctx context.Context, collection string, filter *filter.Filter, funcs ...storage.QueryOptionFunc) ([]storage.Document, error) {
|
||||||
|
opts := &storage.QueryOptions{}
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opts)
|
||||||
|
}
|
||||||
|
|
||||||
var documents []storage.Document
|
var documents []storage.Document
|
||||||
|
|
||||||
err := s.withTx(ctx, func(tx *sql.Tx) error {
|
err := s.withTx(ctx, func(tx *sql.Tx) error {
|
||||||
criteria, args, err := filterSQL.ToSQL(
|
criteria := "1 = 1"
|
||||||
filter.Root(),
|
args := make([]any, 0)
|
||||||
filterSQL.WithPreparedParameter("$", 2),
|
|
||||||
filterSQL.WithKeyTransform(func(key string) string {
|
var err error
|
||||||
return fmt.Sprintf("json_extract(data, '$.%s')", key)
|
|
||||||
}),
|
if filter != nil {
|
||||||
)
|
criteria, args, err = filterSQL.ToSQL(
|
||||||
if err != nil {
|
filter.Root(),
|
||||||
return errors.WithStack(err)
|
filterSQL.WithPreparedParameter("$", 2),
|
||||||
|
filterSQL.WithTransform(transformOperator),
|
||||||
|
filterSQL.WithKeyTransform(func(key string) string {
|
||||||
|
return fmt.Sprintf("json_extract(data, '$.%s')", key)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
@ -112,6 +126,24 @@ func (s *DocumentStore) Query(ctx context.Context, collection string, filter *fi
|
|||||||
|
|
||||||
args = append([]interface{}{collection}, args...)
|
args = append([]interface{}{collection}, args...)
|
||||||
|
|
||||||
|
if opts.OrderBy != nil {
|
||||||
|
query, args = withOrderByClause(query, args, *opts.OrderBy, *opts.OrderDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Offset != nil || opts.Limit != nil {
|
||||||
|
offset := 0
|
||||||
|
if opts.Offset != nil {
|
||||||
|
offset = *opts.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := math.MaxInt
|
||||||
|
if opts.Limit != nil {
|
||||||
|
limit = *opts.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
query, args = withLimitOffsetClause(query, args, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
ctx, "executing query",
|
ctx, "executing query",
|
||||||
logger.F("query", query),
|
logger.F("query", query),
|
||||||
@ -180,12 +212,14 @@ func (s *DocumentStore) Upsert(ctx context.Context, collection string, document
|
|||||||
id = storage.NewDocumentID()
|
id = storage.NewDocumentID()
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(document, storage.DocumentAttrID)
|
|
||||||
delete(document, storage.DocumentAttrCreatedAt)
|
|
||||||
delete(document, storage.DocumentAttrUpdatedAt)
|
|
||||||
|
|
||||||
args := []any{id, collection, JSONMap(document), now, now}
|
args := []any{id, collection, JSONMap(document), now, now}
|
||||||
|
|
||||||
|
logger.Debug(
|
||||||
|
ctx, "executing query",
|
||||||
|
logger.F("query", query),
|
||||||
|
logger.F("args", args),
|
||||||
|
)
|
||||||
|
|
||||||
row := tx.QueryRowContext(ctx, query, args...)
|
row := tx.QueryRowContext(ctx, query, args...)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -321,6 +355,41 @@ func (s *DocumentStore) ensureTables(ctx context.Context, db *sql.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withOrderByClause(query string, args []any, orderBy string, orderDirection storage.OrderDirection) (string, []any) {
|
||||||
|
direction := "ASC"
|
||||||
|
if orderDirection == storage.OrderDirectionDesc {
|
||||||
|
direction = "DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
var column string
|
||||||
|
|
||||||
|
switch orderBy {
|
||||||
|
case storage.DocumentAttrID:
|
||||||
|
column = "id"
|
||||||
|
|
||||||
|
case storage.DocumentAttrCreatedAt:
|
||||||
|
column = "created_at"
|
||||||
|
|
||||||
|
case storage.DocumentAttrUpdatedAt:
|
||||||
|
column = "updated_at"
|
||||||
|
|
||||||
|
default:
|
||||||
|
column = fmt.Sprintf("json_extract(data, '$.' || $%d)", len(args)+1)
|
||||||
|
args = append(args, orderBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
query += fmt.Sprintf(`ORDER BY %s %s`, column, direction)
|
||||||
|
|
||||||
|
return query, args
|
||||||
|
}
|
||||||
|
|
||||||
|
func withLimitOffsetClause(query string, args []any, limit int, offset int) (string, []any) {
|
||||||
|
query += fmt.Sprintf(`LIMIT $%d OFFSET $%d`, len(args)+1, len(args)+2)
|
||||||
|
args = append(args, limit, offset)
|
||||||
|
|
||||||
|
return query, args
|
||||||
|
}
|
||||||
|
|
||||||
func NewDocumentStore(path string) *DocumentStore {
|
func NewDocumentStore(path string) *DocumentStore {
|
||||||
return &DocumentStore{
|
return &DocumentStore{
|
||||||
db: nil,
|
db: nil,
|
||||||
|
50
pkg/storage/sqlite/filter.go
Normal file
50
pkg/storage/sqlite/filter.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/filter/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func transformOperator(operator string, invert bool, key string, value any, option *sql.Option) (string, any, error) {
|
||||||
|
isDataAttr := true
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case storage.DocumentAttrCreatedAt:
|
||||||
|
key = "created_at"
|
||||||
|
isDataAttr = false
|
||||||
|
case storage.DocumentAttrUpdatedAt:
|
||||||
|
key = "updated_at"
|
||||||
|
isDataAttr = false
|
||||||
|
case storage.DocumentAttrID:
|
||||||
|
key = "id"
|
||||||
|
isDataAttr = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDataAttr {
|
||||||
|
option = &sql.Option{
|
||||||
|
PreparedParameter: option.PreparedParameter,
|
||||||
|
KeyTransform: func(key string) string {
|
||||||
|
return key
|
||||||
|
},
|
||||||
|
ValueTransform: option.ValueTransform,
|
||||||
|
Transform: option.Transform,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch operator {
|
||||||
|
case sql.OpIn:
|
||||||
|
return transformInOperator(key, value, option)
|
||||||
|
default:
|
||||||
|
return sql.DefaultTransform(operator, invert, key, value, option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformInOperator(key string, value any, option *sql.Option) (string, any, error) {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"EXISTS (SELECT 1 FROM json_each(json_extract(data, \"$.%v\")) WHERE value = %v)",
|
||||||
|
key,
|
||||||
|
option.PreparedParameter(),
|
||||||
|
), option.ValueTransform(value), nil
|
||||||
|
}
|
@ -7,8 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDocumentStore(t *testing.T, store storage.DocumentStore) {
|
func TestDocumentStore(t *testing.T, store storage.DocumentStore) {
|
||||||
t.Run("Query", func(t *testing.T) {
|
t.Run("Ops", func(t *testing.T) {
|
||||||
// t.Parallel()
|
// t.Parallel()
|
||||||
testDocumentStoreQuery(t, store)
|
testDocumentStoreOps(t, store)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
446
pkg/storage/testsuite/document_store_ops.go
Normal file
446
pkg/storage/testsuite/document_store_ops.go
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
package testsuite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type documentStoreOpsTestCase struct {
|
||||||
|
Name string
|
||||||
|
Run func(ctx context.Context, store storage.DocumentStore) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||||
|
{
|
||||||
|
Name: "Basic query",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
collection := "simple_select"
|
||||||
|
|
||||||
|
docs := []storage.Document{
|
||||||
|
{
|
||||||
|
"attr1": "Foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attr1": "Bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range docs {
|
||||||
|
if _, err := store.Upsert(ctx, collection, d); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := filter.New(
|
||||||
|
filter.NewEqOperator(map[string]interface{}{
|
||||||
|
"attr1": "Foo",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
results, err := store.Query(ctx, collection, filter)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := "Foo", results[0]["attr1"]; e != g {
|
||||||
|
return errors.Errorf("results[0][\"Attr1\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Query on _id",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
collection := "query_on_id"
|
||||||
|
|
||||||
|
doc := storage.Document{
|
||||||
|
"attr1": "Foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertedDoc, err := store.Upsert(ctx, collection, doc)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
docID, ok := upsertedDoc.ID()
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := filter.New(
|
||||||
|
filter.NewEqOperator(map[string]interface{}{
|
||||||
|
"_id": docID,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
results, err := store.Query(ctx, collection, filter)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Query with 'IN' operator",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
docs := []storage.Document{
|
||||||
|
{
|
||||||
|
"counter": 1,
|
||||||
|
"tags": []string{"foo", "bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"counter": 1,
|
||||||
|
"tags": []string{"nope"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := "in_operator"
|
||||||
|
|
||||||
|
for _, doc := range docs {
|
||||||
|
if _, err := store.Upsert(ctx, collection, doc); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := filter.New(
|
||||||
|
filter.NewAndOperator(
|
||||||
|
filter.NewEqOperator(map[string]any{
|
||||||
|
"counter": 1,
|
||||||
|
}),
|
||||||
|
filter.NewInOperator(map[string]any{
|
||||||
|
"tags": "foo",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
results, err := store.Query(ctx, collection, filter)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Double upsert",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
collection := "double_upsert"
|
||||||
|
|
||||||
|
oriDoc := storage.Document{
|
||||||
|
"attr1": "Foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert document for the first time
|
||||||
|
upsertedDoc, err := store.Upsert(ctx, collection, oriDoc)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, exists := upsertedDoc.ID()
|
||||||
|
if !exists {
|
||||||
|
return errors.New("id, exists := upsertedDoc.ID(): 'exists' should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == storage.DocumentID("") {
|
||||||
|
return errors.New("id, exists := upsertedDoc.ID(): 'id' should not be an empty string")
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAt, exists := upsertedDoc.CreatedAt()
|
||||||
|
if !exists {
|
||||||
|
return errors.New("createdAt, exists := upsertedDoc.CreatedAt(): 'exists' should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if createdAt.IsZero() {
|
||||||
|
return errors.New("createdAt, exists := upsertedDoc.CreatedAt(): 'createdAt' should not be zero time")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt, exists := upsertedDoc.UpdatedAt()
|
||||||
|
if !exists {
|
||||||
|
return errors.New("updatedAt, exists := upsertedDoc.UpdatedAt(): 'exists' should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedAt.IsZero() {
|
||||||
|
return errors.New("updatedAt, exists := upsertedDoc.UpdatedAt(): 'updatedAt' should not be zero time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := oriDoc["attr1"], upsertedDoc["attr1"]; e != g {
|
||||||
|
return errors.Errorf("upsertedDoc[\"attr1\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that document does not have unexpected properties
|
||||||
|
if e, g := 4, len(upsertedDoc); e != g {
|
||||||
|
return errors.Errorf("len(upsertedDoc): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert document for the second time
|
||||||
|
upsertedDoc2, err := store.Upsert(ctx, collection, upsertedDoc)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevID, _ := upsertedDoc.ID()
|
||||||
|
newID, _ := upsertedDoc2.ID()
|
||||||
|
|
||||||
|
if e, g := prevID, newID; e != g {
|
||||||
|
return errors.Errorf("newID: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAt1, _ := upsertedDoc.CreatedAt()
|
||||||
|
createdAt2, _ := upsertedDoc2.CreatedAt()
|
||||||
|
|
||||||
|
if e, g := createdAt1, createdAt2; e != g {
|
||||||
|
return errors.Errorf("upsertedDoc2.CreatedAt(): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt1, _ := upsertedDoc.UpdatedAt()
|
||||||
|
updatedAt2, _ := upsertedDoc2.UpdatedAt()
|
||||||
|
|
||||||
|
if e, g := updatedAt1, updatedAt2; e == g {
|
||||||
|
return errors.New("upsertedDoc2.UpdatedAt() should have been different than upsertedDoc.UpdatedAt()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that there is no additional created document in the collection
|
||||||
|
|
||||||
|
results, err := store.Query(ctx, collection, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Query order by document field",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
docs := []storage.Document{
|
||||||
|
{
|
||||||
|
"sortedField": 0,
|
||||||
|
"name": "Item 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sortedField": 1,
|
||||||
|
"name": "Item 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sortedField": 2,
|
||||||
|
"name": "Item 3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := "ordered_query_by_document_field"
|
||||||
|
|
||||||
|
for _, doc := range docs {
|
||||||
|
if _, err := store.Upsert(ctx, collection, doc); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := store.Query(
|
||||||
|
ctx, collection, nil,
|
||||||
|
storage.WithOrderBy("sortedField"),
|
||||||
|
storage.WithOrderDirection(storage.OrderDirectionAsc),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 3, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[0]["name"], results[0]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[2]["name"], results[2]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err = store.Query(
|
||||||
|
ctx, collection, nil,
|
||||||
|
storage.WithOrderBy("sortedField"),
|
||||||
|
storage.WithOrderDirection(storage.OrderDirectionDesc),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 3, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[2]["name"], results[0]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[0]["name"], results[2]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Query order by special attr",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
docs := []storage.Document{
|
||||||
|
{
|
||||||
|
"name": "Item 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Item 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Item 3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := "ordered_query_by_special_attr"
|
||||||
|
|
||||||
|
for _, doc := range docs {
|
||||||
|
if _, err := store.Upsert(ctx, collection, doc); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := store.Query(
|
||||||
|
ctx, collection, nil,
|
||||||
|
storage.WithOrderBy(storage.DocumentAttrCreatedAt),
|
||||||
|
storage.WithOrderDirection(storage.OrderDirectionAsc),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 3, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[0]["name"], results[0]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[2]["name"], results[2]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err = store.Query(
|
||||||
|
ctx, collection, nil,
|
||||||
|
storage.WithOrderBy(storage.DocumentAttrCreatedAt),
|
||||||
|
storage.WithOrderDirection(storage.OrderDirectionDesc),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 3, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[2]["name"], results[0]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[0]["name"], results[2]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Query limit and offset",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
docs := []storage.Document{
|
||||||
|
{"name": "Item 1"},
|
||||||
|
{"name": "Item 2"},
|
||||||
|
{"name": "Item 3"},
|
||||||
|
{"name": "Item 4"},
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := "query_limit_and_offset"
|
||||||
|
|
||||||
|
for _, doc := range docs {
|
||||||
|
if _, err := store.Upsert(ctx, collection, doc); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := store.Query(
|
||||||
|
ctx, collection, nil,
|
||||||
|
storage.WithLimit(2),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 2, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[0]["name"], results[0]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[1]["name"], results[1]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[1][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err = store.Query(
|
||||||
|
ctx, collection, nil,
|
||||||
|
storage.WithOffset(2),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 2, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[2]["name"], results[0]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := docs[3]["name"], results[1]["name"]; e != g {
|
||||||
|
return errors.Errorf("results[1][\"name\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDocumentStoreOps(t *testing.T, store storage.DocumentStore) {
|
||||||
|
for _, tc := range documentStoreOpsTestCases {
|
||||||
|
func(tc documentStoreOpsTestCase) {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
if err := tc.Run(context.Background(), store); err != nil {
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}(tc)
|
||||||
|
}
|
||||||
|
}
|
@ -1,85 +0,0 @@
|
|||||||
package testsuite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type documentStoreQueryTestCase struct {
|
|
||||||
Name string
|
|
||||||
Before func(ctx context.Context, store storage.DocumentStore) error
|
|
||||||
Collection string
|
|
||||||
Filter *filter.Filter
|
|
||||||
QueryOptionsFuncs []storage.QueryOptionFunc
|
|
||||||
After func(t *testing.T, results []storage.Document, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var documentStoreQueryTestCases = []documentStoreQueryTestCase{
|
|
||||||
{
|
|
||||||
Name: "Simple select",
|
|
||||||
Before: func(ctx context.Context, store storage.DocumentStore) error {
|
|
||||||
doc1 := storage.Document{
|
|
||||||
"attr1": "Foo",
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := store.Upsert(ctx, "simple_select", doc1); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
doc2 := storage.Document{
|
|
||||||
"attr1": "Bar",
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := store.Upsert(ctx, "simple_select", doc2); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Collection: "simple_select",
|
|
||||||
Filter: filter.New(
|
|
||||||
filter.NewEqOperator(map[string]interface{}{
|
|
||||||
"attr1": "Foo",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
After: func(t *testing.T, results []storage.Document, err error) {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if e, g := 1, len(results); e != g {
|
|
||||||
t.Errorf("len(results): expected '%v', got '%v'", e, g)
|
|
||||||
}
|
|
||||||
|
|
||||||
if e, g := "Foo", results[0]["attr1"]; e != g {
|
|
||||||
t.Errorf("results[0][\"Attr1\"]: expected '%v', got '%v'", e, g)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDocumentStoreQuery(t *testing.T, store storage.DocumentStore) {
|
|
||||||
for _, tc := range documentStoreQueryTestCases {
|
|
||||||
func(tc documentStoreQueryTestCase) {
|
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
|
||||||
// t.Parallel()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if tc.Before != nil {
|
|
||||||
if err := tc.Before(ctx, store); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
documents, err := store.Query(ctx, tc.Collection, tc.Filter, tc.QueryOptionsFuncs...)
|
|
||||||
|
|
||||||
tc.After(t, documents, err)
|
|
||||||
})
|
|
||||||
}(tc)
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user