Compare commits

...

4 Commits

Author SHA1 Message Date
8e574c299b feat(storage): rpc based implementation
All checks were successful
arcad/edge/pipeline/pr-master This commit looks good
2023-09-28 12:36:30 -06:00
c3535a4a9b feat(http): allow passing middlewares via options
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-09-20 09:23:53 -06:00
7e58551f6a docs(context): remove reference to obsolete attribute
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-09-20 09:02:27 -06:00
41d5db6321 docs(auth): add informations about anonymous users
ref arcad/edge-menu#86
2023-09-20 09:01:36 -06:00
117 changed files with 3033 additions and 273 deletions

View File

@ -1 +1,3 @@
RUN_APP_ARGS="" RUN_APP_ARGS=""
#EDGE_DOCUMENTSTORE_DSN="rpc://localhost:3001/documentstore?tenant=local&appId=%APPID%"
#EDGE_BLOBSTORE_DSN="rpc://localhost:3001/blobstore?tenant=local&appId=%APPID%"

3
.gitignore vendored
View File

@ -4,4 +4,5 @@
/tools /tools
*.sqlite *.sqlite
/.gitea-release /.gitea-release
/.edge /.edge
/data

View File

@ -11,9 +11,12 @@ DATE_VERSION := $(shell date +%Y.%-m.%-d)
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,) FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
APP_PATH ?= misc/client-sdk-testsuite/dist APP_PATH ?= misc/client-sdk-testsuite/dist
RUN_APP_ARGS ?= RUN_APP_ARGS ?=
RUN_STORAGE_SERVER_ARGS ?=
SHELL := bash SHELL := bash
build: build-edge-cli build-client-sdk-test-app
build: build-cli build-storage-server build-client-sdk-test-app
watch: tools/modd/bin/modd watch: tools/modd/bin/modd
tools/modd/bin/modd tools/modd/bin/modd
@ -22,17 +25,23 @@ watch: tools/modd/bin/modd
test: test-go test: test-go
test-go: test-go:
go test -v -count=1 $(GOTEST_ARGS) ./... go test -count=1 $(GOTEST_ARGS) ./...
lint: lint:
golangci-lint run --enable-all $(LINT_ARGS) golangci-lint run --enable-all $(LINT_ARGS)
build-edge-cli: build-sdk build-cli: build-sdk
CGO_ENABLED=0 go build \ CGO_ENABLED=0 go build \
-v \ -v \
-o ./bin/cli \ -o ./bin/cli \
./cmd/cli ./cmd/cli
build-storage-server: build-sdk
CGO_ENABLED=0 go build \
-v \
-o ./bin/storage-server \
./cmd/storage-server
build-client-sdk-test-app: build-client-sdk-test-app:
cd misc/client-sdk-testsuite && $(MAKE) dist cd misc/client-sdk-testsuite && $(MAKE) dist
@ -68,6 +77,9 @@ node_modules:
run-app: .env run-app: .env
( set -o allexport && source .env && set +o allexport && bin/cli app run -p $(APP_PATH) $$RUN_APP_ARGS ) ( set -o allexport && source .env && set +o allexport && bin/cli app run -p $(APP_PATH) $$RUN_APP_ARGS )
run-storage-server: .env
( set -o allexport && source .env && set +o allexport && bin/storage-server run $$RUN_STORAGE_SERVER_ARGS )
.env: .env:
cp .env.dist .env cp .env.dist .env

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -28,9 +27,7 @@ import (
"forge.cadoles.com/arcad/edge/pkg/module/fetch" "forge.cadoles.com/arcad/edge/pkg/module/fetch"
netModule "forge.cadoles.com/arcad/edge/pkg/module/net" netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
shareModule "forge.cadoles.com/arcad/edge/pkg/module/share" shareModule "forge.cadoles.com/arcad/edge/pkg/module/share"
shareSqlite "forge.cadoles.com/arcad/edge/pkg/module/share/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage" "forge.cadoles.com/arcad/edge/pkg/storage"
storageSqlite "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"
@ -45,6 +42,12 @@ import (
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id" _ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain" _ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
// Register storage drivers
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
) )
func RunCommand() *cli.Command { func RunCommand() *cli.Command {
@ -75,14 +78,22 @@ func RunCommand() *cli.Command {
Value: 0, Value: 0,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "storage-file", Name: "blobstore-dsn",
Usage: "use `FILE` for SQLite storage database", Usage: "use `DSN` for blob storage",
Value: ".edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000", EnvVars: []string{"EDGE_BLOBSTORE_DSN"},
Value: "sqlite://.edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "shared-resources-file", Name: "documentstore-dsn",
Usage: "use `FILE` for SQLite shared resources database", Usage: "use `DSN` for document storage",
Value: ".edge/shared-resources.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000", EnvVars: []string{"EDGE_DOCUMENTSTORE_DSN"},
Value: "sqlite://.edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
},
&cli.StringFlag{
Name: "sharestore-dsn",
Usage: "use `DSN` for share storage",
EnvVars: []string{"EDGE_SHARESTORE_DSN"},
Value: "sqlite://.edge/share.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "accounts-file", Name: "accounts-file",
@ -96,9 +107,10 @@ func RunCommand() *cli.Command {
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") blobstoreDSN := ctx.String("blobstore-dsn")
documentstoreDSN := ctx.String("documentstore-dsn")
shareStoreDSN := ctx.String("sharestore-dsn")
accountsFile := ctx.String("accounts-file") accountsFile := ctx.String("accounts-file")
sharedResourcesFile := ctx.String("shared-resources-file")
logger.SetFormat(logger.Format(logFormat)) logger.SetFormat(logger.Format(logFormat))
logger.SetLevel(logger.Level(logLevel)) logger.SetLevel(logger.Level(logLevel))
@ -144,7 +156,7 @@ func RunCommand() *cli.Command {
appCtx := logger.With(cmdCtx, logger.F("address", address)) appCtx := logger.With(cmdCtx, logger.F("address", address))
if err := runApp(appCtx, path, address, storageFile, accountsFile, appsRepository, sharedResourcesFile); err != nil { if err := runApp(appCtx, path, address, documentstoreDSN, blobstoreDSN, shareStoreDSN, accountsFile, appsRepository); err != nil {
logger.Error(appCtx, "could not run app", logger.E(errors.WithStack(err))) logger.Error(appCtx, "could not run app", logger.E(errors.WithStack(err)))
} }
}(p, port, idx) }(p, port, idx)
@ -157,7 +169,7 @@ func RunCommand() *cli.Command {
} }
} }
func runApp(ctx context.Context, path string, address string, storageFile string, accountsFile string, appRepository appModule.Repository, sharedResourcesFile string) error { func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN, shareStoreDSN, accountsFile string, appRepository appModule.Repository) error {
absPath, err := filepath.Abs(path) absPath, err := filepath.Abs(path)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not resolve path '%s'", path) return errors.Wrapf(err, "could not resolve path '%s'", path)
@ -190,9 +202,8 @@ func runApp(ctx context.Context, path string, address string, storageFile string
deps := &moduleDeps{} deps := &moduleDeps{}
funcs := []ModuleDepFunc{ funcs := []ModuleDepFunc{
initMemoryBus, initMemoryBus,
initDatastores(storageFile, manifest.ID), initDatastores(documentStoreDSN, blobStoreDSN, shareStoreDSN, manifest.ID),
initAccounts(accountsFile, manifest.ID), initAccounts(accountsFile, manifest.ID),
initShareRepository(sharedResourcesFile),
initAppRepository(appRepository), initAppRepository(appRepository),
} }
@ -216,15 +227,17 @@ func runApp(ctx context.Context, path string, address string, storageFile string
authModule.WithJWT(dummyKeySet), authModule.WithJWT(dummyKeySet),
), ),
), ),
appHTTP.WithHTTPMiddlewares(
authModuleMiddleware.AnonymousUser(
jwa.HS256, key,
),
),
) )
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")
} }
router := chi.NewRouter() router := chi.NewRouter()
router.Use(authModuleMiddleware.AnonymousUser(
jwa.HS256, key,
))
router.Use(middleware.Logger) router.Use(middleware.Logger)
router.Use(middleware.Compress(5)) router.Use(middleware.Compress(5))
@ -241,13 +254,13 @@ func runApp(ctx context.Context, path string, address string, storageFile string
} }
type moduleDeps struct { type moduleDeps struct {
AppID app.ID AppID app.ID
Bus bus.Bus Bus bus.Bus
DocumentStore storage.DocumentStore DocumentStore storage.DocumentStore
BlobStore storage.BlobStore BlobStore storage.BlobStore
AppRepository appModule.Repository AppRepository appModule.Repository
ShareRepository shareModule.Repository ShareStore share.Store
Accounts []authHTTP.LocalAccount Accounts []authHTTP.LocalAccount
} }
type ModuleDepFunc func(*moduleDeps) error type ModuleDepFunc func(*moduleDeps) error
@ -267,7 +280,7 @@ func getServerModules(deps *moduleDeps) []app.ServerModuleFactory {
), ),
appModule.ModuleFactory(deps.AppRepository), appModule.ModuleFactory(deps.AppRepository),
fetch.ModuleFactory(deps.Bus), fetch.ModuleFactory(deps.Bus),
shareModule.ModuleFactory(deps.AppID, deps.ShareRepository), shareModule.ModuleFactory(deps.AppID, deps.ShareStore),
} }
} }
@ -321,10 +334,10 @@ func loadLocalAccounts(path string) ([]authHTTP.LocalAccount, error) {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
data, err := ioutil.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
if err := ioutil.WriteFile(path, defaultAccounts, 0o640); err != nil { if err := os.WriteFile(path, defaultAccounts, 0o640); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -435,21 +448,32 @@ func initMemoryBus(deps *moduleDeps) error {
return nil return nil
} }
func initDatastores(storageFile string, appID app.ID) ModuleDepFunc { func initDatastores(documentStoreDSN, blobStoreDSN, shareStoreDSN string, appID app.ID) ModuleDepFunc {
return func(deps *moduleDeps) error { return func(deps *moduleDeps) error {
storageFile = injectAppID(storageFile, appID) documentStoreDSN = injectAppID(documentStoreDSN, appID)
if err := ensureDir(storageFile); err != nil { documentStore, err := driver.NewDocumentStore(documentStoreDSN)
return errors.WithStack(err)
}
db, err := storageSqlite.Open(storageFile)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
deps.DocumentStore = storageSqlite.NewDocumentStoreWithDB(db) deps.DocumentStore = documentStore
deps.BlobStore = storageSqlite.NewBlobStoreWithDB(db)
blobStoreDSN = injectAppID(blobStoreDSN, appID)
blobStore, err := driver.NewBlobStore(blobStoreDSN)
if err != nil {
return errors.WithStack(err)
}
deps.BlobStore = blobStore
shareStore, err := driver.NewShareStore(shareStoreDSN)
if err != nil {
return errors.WithStack(err)
}
deps.ShareStore = shareStore
return nil return nil
} }
@ -469,17 +493,3 @@ func initAccounts(accountsFile string, appID app.ID) ModuleDepFunc {
return nil return nil
} }
} }
func initShareRepository(shareRepositoryFile string) ModuleDepFunc {
return func(deps *moduleDeps) error {
if err := ensureDir(shareRepositoryFile); err != nil {
return errors.WithStack(err)
}
repo := shareSqlite.NewRepository(shareRepositoryFile)
deps.ShareRepository = repo
return nil
}
}

View File

@ -0,0 +1,13 @@
package auth
import (
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "auth",
Usage: "Auth related command",
Subcommands: []*cli.Command{},
}
}

View File

@ -0,0 +1,48 @@
package command
import (
"context"
"fmt"
"os"
"sort"
"github.com/urfave/cli/v2"
)
func Main(commands ...*cli.Command) {
ctx := context.Background()
app := &cli.App{
Name: "storage-server",
Usage: "Edge storage server",
Commands: commands,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "debug",
EnvVars: []string{"DEBUG"},
Value: false,
},
},
}
app.ExitErrHandler = func(ctx *cli.Context, err error) {
if err == nil {
return
}
debug := ctx.Bool("debug")
if !debug {
fmt.Printf("[ERROR] %v\n", err)
} else {
fmt.Printf("%+v", err)
}
}
sort.Sort(cli.FlagsByName(app.Flags))
sort.Sort(cli.CommandsByName(app.Commands))
if err := app.RunContext(ctx, os.Args); err != nil {
os.Exit(1)
}
}

View File

@ -0,0 +1,179 @@
package command
import (
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/keegancsmith/rpc"
"gitlab.com/wpetit/goweb/logger"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
// Register storage drivers
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
)
func Run() *cli.Command {
return &cli.Command{
Name: "run",
Usage: "Run server",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Aliases: []string{"addr"},
Value: ":3001",
},
&cli.StringFlag{
Name: "blobstore-dsn-pattern",
EnvVars: []string{"STORAGE_SERVER_BLOBSTORE_DSN_PATTERN"},
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/blobstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
},
&cli.StringFlag{
Name: "documentstore-dsn-pattern",
EnvVars: []string{"STORAGE_SERVER_DOCUMENTSTORE_DSN_PATTERN"},
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/documentstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
},
&cli.StringFlag{
Name: "sharestore-dsn-pattern",
EnvVars: []string{"STORAGE_SERVER_SHARESTORE_DSN_PATTERN"},
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/sharestore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
},
&cli.DurationFlag{
Name: "cache-ttl",
EnvVars: []string{"STORAGE_SERVER_CACHE_TTL"},
Value: time.Hour,
},
&cli.IntFlag{
Name: "cache-size",
EnvVars: []string{"STORAGE_SERVER_CACHE_SIZE"},
Value: 32,
},
},
Action: func(ctx *cli.Context) error {
addr := ctx.String("address")
blobStoreDSNPattern := ctx.String("blobstore-dsn-pattern")
documentStoreDSNPattern := ctx.String("documentstore-dsn-pattern")
shareStoreDSNPattern := ctx.String("sharestore-dsn-pattern")
cacheSize := ctx.Int("cache-size")
cacheTTL := ctx.Duration("cache-ttl")
router := chi.NewRouter()
getBlobStoreServer := createGetCachedStoreServer(
func(dsn string) (storage.BlobStore, error) {
return driver.NewBlobStore(dsn)
},
func(store storage.BlobStore) *rpc.Server {
return server.NewBlobStoreServer(store)
},
)
getShareStoreServer := createGetCachedStoreServer(
func(dsn string) (share.Store, error) {
return driver.NewShareStore(dsn)
},
func(store share.Store) *rpc.Server {
return server.NewShareStoreServer(store)
},
)
getDocumentStoreServer := createGetCachedStoreServer(
func(dsn string) (storage.DocumentStore, error) {
return driver.NewDocumentStore(dsn)
},
func(store storage.DocumentStore) *rpc.Server {
return server.NewDocumentStoreServer(store)
},
)
router.Use(middleware.RealIP)
router.Use(middleware.Logger)
router.Handle("/blobstore", createStoreHandler(getBlobStoreServer, blobStoreDSNPattern, cacheSize, cacheTTL))
router.Handle("/documentstore", createStoreHandler(getDocumentStoreServer, documentStoreDSNPattern, cacheSize, cacheTTL))
router.Handle("/sharestore", createStoreHandler(getShareStoreServer, shareStoreDSNPattern, cacheSize, cacheTTL))
if err := http.ListenAndServe(addr, router); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}
type getRPCServerFunc func(cacheSize int, cacheTTL time.Duration, tenant, appID, dsnPattern string) (*rpc.Server, error)
func createGetCachedStoreServer[T any](storeFactory func(dsn string) (T, error), serverFactory func(store T) *rpc.Server) getRPCServerFunc {
var (
cache *expirable.LRU[string, *rpc.Server]
initCache sync.Once
)
return func(cacheSize int, cacheTTL time.Duration, tenant, appID, dsnPattern string) (*rpc.Server, error) {
initCache.Do(func() {
cache = expirable.NewLRU[string, *rpc.Server](cacheSize, nil, cacheTTL)
})
key := fmt.Sprintf("%s:%s", tenant, appID)
storeServer, _ := cache.Get(key)
if storeServer != nil {
return storeServer, nil
}
dsn := strings.ReplaceAll(dsnPattern, "%TENANT%", tenant)
dsn = strings.ReplaceAll(dsn, "%APPID%", appID)
store, err := storeFactory(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
storeServer = serverFactory(store)
cache.Add(key, storeServer)
return storeServer, nil
}
}
func createStoreHandler(getStoreServer getRPCServerFunc, dsnPattern string, cacheSize int, cacheTTL time.Duration) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenant := r.URL.Query().Get("tenant")
if tenant == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
appID := r.URL.Query().Get("appId")
if tenant == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
server, err := getStoreServer(cacheSize, cacheTTL, tenant, appID, dsnPattern)
if err != nil {
logger.Error(r.Context(), "could not retrieve store server", logger.E(errors.WithStack(err)), logger.F("tenant", tenant))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
server.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,13 @@
package main
import (
"forge.cadoles.com/arcad/edge/cmd/storage-server/command"
"forge.cadoles.com/arcad/edge/cmd/storage-server/command/auth"
)
func main() {
command.Main(
command.Run(),
auth.Root(),
)
}

View File

@ -2,6 +2,14 @@
Ce module permet de récupérer des informations concernant l'utilisateur connecté et ses attributs. Ce module permet de récupérer des informations concernant l'utilisateur connecté et ses attributs.
### Utilisateurs anonymes
Edge génère automatiquement une session pour les utilisateurs anonymes. Ainsi, qu'un utilisateur soit identifié ou non les `claims` suivants seront toujours valués:
- `auth.CLAIM_SUBJECT`
- `auth.CLAIM_PREFERRED_USERNAME`
- `auth.CLAIM_ISSUER` (prendra la valeur `anon` dans le cas d'un utilisateur anonyme)
## Méthodes ## Méthodes
### `auth.getClaim(ctx: Context, name: string): string` ### `auth.getClaim(ctx: Context, name: string): string`

View File

@ -43,12 +43,6 @@ function onClientMessage(ctx, message) {
} }
``` ```
## Propriétés
### `context.SESSION_ID`
Clé permettant de récupérer la clé de session associé au client émetteur du message courant.
#### Usage #### Usage
```js ```js

2
go.mod
View File

@ -15,6 +15,8 @@ require (
github.com/go-playground/universal-translator v0.16.0 // indirect github.com/go-playground/universal-translator v0.16.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect github.com/goccy/go-json v0.9.11 // indirect
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
github.com/hashicorp/golang-lru/v2 v2.0.6 // indirect
github.com/keegancsmith/rpc v1.3.0 // indirect
github.com/leodido/go-urn v1.1.0 // indirect github.com/leodido/go-urn v1.1.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect

4
go.sum
View File

@ -188,6 +188,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 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/golang-lru/v2 v2.0.6 h1:3xi/Cafd1NaoEnS/yDssIiuVeDVywU0QdFGl3aQaQHM=
github.com/hashicorp/golang-lru/v2 v2.0.6/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
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/go.mod h1:aa76Av3qgPeIQp9Y3qIkTBPieQYNkQ13Kxe7pze9Wb0= github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8/go.mod h1:aa76Av3qgPeIQp9Y3qIkTBPieQYNkQ13Kxe7pze9Wb0=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE= github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
@ -201,6 +203,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/keegancsmith/rpc v1.3.0 h1:wGWOpjcNrZaY8GDYZJfvyxmlLljm3YQWF+p918DXtDk=
github.com/keegancsmith/rpc v1.3.0/go.mod h1:6O2xnOGjPyvIPbvp0MdrOe5r6cu1GZ4JoTzpzDhWeo0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=

View File

@ -9,6 +9,7 @@ modd.conf
prep: make build-client-sdk-test-app prep: make build-client-sdk-test-app
prep: make build prep: make build
daemon: make run-app daemon: make run-app
daemon: make run-storage-server
} }
**/*.go { **/*.go {

View File

@ -97,6 +97,10 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
bus: opts.Bus, bus: opts.Bus,
} }
for _, middleware := range opts.HTTPMiddlewares {
router.Use(middleware)
}
router.Route("/edge", func(r chi.Router) { router.Route("/edge", func(r chi.Router) {
r.Route("/sdk", func(r chi.Router) { r.Route("/sdk", func(r chi.Router) {
r.Get("/client.js", handler.handleSDKClient) r.Get("/client.js", handler.handleSDKClient)

View File

@ -18,6 +18,7 @@ type HandlerOptions struct {
UploadMaxFileSize int64 UploadMaxFileSize int64
HTTPClient *http.Client HTTPClient *http.Client
HTTPMounts []func(r chi.Router) HTTPMounts []func(r chi.Router)
HTTPMiddlewares []func(next http.Handler) http.Handler
} }
func defaultHandlerOptions() *HandlerOptions { func defaultHandlerOptions() *HandlerOptions {
@ -34,7 +35,8 @@ func defaultHandlerOptions() *HandlerOptions {
HTTPClient: &http.Client{ HTTPClient: &http.Client{
Timeout: time.Second * 30, Timeout: time.Second * 30,
}, },
HTTPMounts: make([]func(r chi.Router), 0), HTTPMounts: make([]func(r chi.Router), 0),
HTTPMiddlewares: make([]func(http.Handler) http.Handler, 0),
} }
} }
@ -75,3 +77,9 @@ func WithHTTPMounts(mounts ...func(r chi.Router)) HandlerOptionFunc {
opts.HTTPMounts = mounts opts.HTTPMounts = mounts
} }
} }
func WithHTTPMiddlewares(middlewares ...func(http.Handler) http.Handler) HandlerOptionFunc {
return func(opts *HandlerOptions) {
opts.HTTPMiddlewares = middlewares
}
}

View File

@ -1,14 +1,14 @@
package blob package blob
import ( import (
"io/ioutil" "os"
"testing" "testing"
"cdr.dev/slog" "cdr.dev/slog"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus/memory" "forge.cadoles.com/arcad/edge/pkg/bus/memory"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite" "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -27,7 +27,7 @@ func TestBlobModule(t *testing.T) {
ModuleFactory(bus, store), ModuleFactory(bus, store),
) )
data, err := ioutil.ReadFile("testdata/blob.js") data, err := os.ReadFile("testdata/blob.js")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -5,18 +5,19 @@ import (
"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/module/util"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const ( const (
AnyType ValueType = "*" AnyType share.ValueType = "*"
AnyName string = "*" AnyName string = "*"
) )
type Module struct { type Module struct {
appID app.ID appID app.ID
repository Repository store share.Store
} }
func (m *Module) Name() string { func (m *Module) Name() string {
@ -48,19 +49,19 @@ func (m *Module) Export(export *goja.Object) {
panic(errors.Wrap(err, "could not set 'ANY_NAME' property")) panic(errors.Wrap(err, "could not set 'ANY_NAME' property"))
} }
if err := export.Set("TYPE_TEXT", TypeText); err != nil { if err := export.Set("TYPE_TEXT", share.TypeText); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_TEXT' property")) panic(errors.Wrap(err, "could not set 'TYPE_TEXT' property"))
} }
if err := export.Set("TYPE_NUMBER", TypeNumber); err != nil { if err := export.Set("TYPE_NUMBER", share.TypeNumber); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_NUMBER' property")) panic(errors.Wrap(err, "could not set 'TYPE_NUMBER' property"))
} }
if err := export.Set("TYPE_BOOL", TypeBool); err != nil { if err := export.Set("TYPE_BOOL", share.TypeBool); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_BOOL' property")) panic(errors.Wrap(err, "could not set 'TYPE_BOOL' property"))
} }
if err := export.Set("TYPE_PATH", TypePath); err != nil { if err := export.Set("TYPE_PATH", share.TypePath); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_PATH' property")) panic(errors.Wrap(err, "could not set 'TYPE_PATH' property"))
} }
} }
@ -69,20 +70,20 @@ func (m *Module) upsertResource(call goja.FunctionCall, rt *goja.Runtime) goja.V
ctx := util.AssertContext(call.Argument(0), rt) ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt) resourceID := assertResourceID(call.Argument(1), rt)
var attributes []Attribute var attributes []share.Attribute
if len(call.Arguments) > 2 { if len(call.Arguments) > 2 {
attributes = assertAttributes(call.Arguments[2:], rt) attributes = assertAttributes(call.Arguments[2:], rt)
} else { } else {
attributes = make([]Attribute, 0) attributes = make([]share.Attribute, 0)
} }
for _, attr := range attributes { for _, attr := range attributes {
if err := AssertType(attr.Value(), attr.Type()); err != nil { if err := share.AssertType(attr.Value(), attr.Type()); err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
} }
resource, err := m.repository.UpdateAttributes(ctx, m.appID, resourceID, attributes...) resource, err := m.store.UpdateAttributes(ctx, m.appID, resourceID, attributes...)
if err != nil { if err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
@ -101,7 +102,7 @@ func (m *Module) deleteAttributes(call goja.FunctionCall, rt *goja.Runtime) goja
names = make([]string, 0) names = make([]string, 0)
} }
err := m.repository.DeleteAttributes(ctx, m.appID, resourceID, names...) err := m.store.DeleteAttributes(ctx, m.appID, resourceID, names...)
if err != nil { if err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
@ -112,23 +113,23 @@ func (m *Module) deleteAttributes(call goja.FunctionCall, rt *goja.Runtime) goja
func (m *Module) findResources(call goja.FunctionCall, rt *goja.Runtime) goja.Value { func (m *Module) findResources(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt) ctx := util.AssertContext(call.Argument(0), rt)
funcs := make([]FindResourcesOptionFunc, 0) funcs := make([]share.FindResourcesOptionFunc, 0)
if len(call.Arguments) > 1 { if len(call.Arguments) > 1 {
name := util.AssertString(call.Argument(1), rt) name := util.AssertString(call.Argument(1), rt)
if name != AnyName { if name != AnyName {
funcs = append(funcs, WithName(name)) funcs = append(funcs, share.WithName(name))
} }
} }
if len(call.Arguments) > 2 { if len(call.Arguments) > 2 {
valueType := assertValueType(call.Argument(2), rt) valueType := assertValueType(call.Argument(2), rt)
if valueType != AnyType { if valueType != AnyType {
funcs = append(funcs, WithType(valueType)) funcs = append(funcs, share.WithType(valueType))
} }
} }
resources, err := m.repository.FindResources(ctx, funcs...) resources, err := m.store.FindResources(ctx, funcs...)
if err != nil { if err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
@ -140,7 +141,7 @@ func (m *Module) deleteResource(call goja.FunctionCall, rt *goja.Runtime) goja.V
ctx := util.AssertContext(call.Argument(0), rt) ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt) resourceID := assertResourceID(call.Argument(1), rt)
err := m.repository.DeleteResource(ctx, m.appID, resourceID) err := m.store.DeleteResource(ctx, m.appID, resourceID)
if err != nil { if err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
@ -148,29 +149,29 @@ func (m *Module) deleteResource(call goja.FunctionCall, rt *goja.Runtime) goja.V
return nil return nil
} }
func ModuleFactory(appID app.ID, repository Repository) app.ServerModuleFactory { func ModuleFactory(appID app.ID, store share.Store) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule { return func(server *app.Server) app.ServerModule {
return &Module{ return &Module{
appID: appID, appID: appID,
repository: repository, store: store,
} }
} }
} }
func assertResourceID(v goja.Value, r *goja.Runtime) ResourceID { func assertResourceID(v goja.Value, r *goja.Runtime) share.ResourceID {
value := v.Export() value := v.Export()
switch typ := value.(type) { switch typ := value.(type) {
case string: case string:
return ResourceID(typ) return share.ResourceID(typ)
case ResourceID: case share.ResourceID:
return typ return typ
default: default:
panic(r.ToValue(errors.Errorf("expected value to be a string or ResourceID, got '%T'", value))) panic(r.ToValue(errors.Errorf("expected value to be a string or ResourceID, got '%T'", value)))
} }
} }
func assertAttributes(values []goja.Value, r *goja.Runtime) []Attribute { func assertAttributes(values []goja.Value, r *goja.Runtime) []share.Attribute {
attributes := make([]Attribute, len(values)) attributes := make([]share.Attribute, len(values))
for idx, val := range values { for idx, val := range values {
export := val.Export() export := val.Export()
@ -195,12 +196,12 @@ func assertAttributes(values []goja.Value, r *goja.Runtime) []Attribute {
panic(r.ToValue(errors.Errorf("could not find 'type' property on attribute '%v'", export))) panic(r.ToValue(errors.Errorf("could not find 'type' property on attribute '%v'", export)))
} }
var valueType ValueType var valueType share.ValueType
switch typ := rawType.(type) { switch typ := rawType.(type) {
case ValueType: case share.ValueType:
valueType = typ valueType = typ
case string: case string:
valueType = ValueType(typ) valueType = share.ValueType(typ)
default: default:
panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'type': expected 'string' or 'ValueType', got '%T'", rawType))) panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'type': expected 'string' or 'ValueType', got '%T'", rawType)))
@ -211,7 +212,7 @@ func assertAttributes(values []goja.Value, r *goja.Runtime) []Attribute {
panic(r.ToValue(errors.Errorf("could not find 'value' property on attribute '%v'", export))) panic(r.ToValue(errors.Errorf("could not find 'value' property on attribute '%v'", export)))
} }
attributes[idx] = NewBaseAttribute( attributes[idx] = share.NewBaseAttribute(
name, name,
valueType, valueType,
value, value,
@ -232,12 +233,12 @@ func assertStrings(values []goja.Value, r *goja.Runtime) []string {
return strings return strings
} }
func assertValueType(v goja.Value, r *goja.Runtime) ValueType { func assertValueType(v goja.Value, r *goja.Runtime) share.ValueType {
value := v.Export() value := v.Export()
switch typ := value.(type) { switch typ := value.(type) {
case string: case string:
return ValueType(typ) return share.ValueType(typ)
case ValueType: case share.ValueType:
return typ return typ
default: default:
panic(r.ToValue(errors.Errorf("expected value to be a string or ValueType, got '%T'", value))) panic(r.ToValue(errors.Errorf("expected value to be a string or ValueType, got '%T'", value)))
@ -245,7 +246,7 @@ func assertValueType(v goja.Value, r *goja.Runtime) ValueType {
} }
type gojaResource struct { type gojaResource struct {
ID ResourceID `goja:"id" json:"id"` ID share.ResourceID `goja:"id" json:"id"`
Origin app.ID `goja:"origin" json:"origin"` Origin app.ID `goja:"origin" json:"origin"`
Attributes []*gojaAttribute `goja:"attributes" json:"attributes"` Attributes []*gojaAttribute `goja:"attributes" json:"attributes"`
} }
@ -254,7 +255,7 @@ func (r *gojaResource) Has(call goja.FunctionCall, rt *goja.Runtime) goja.Value
name := util.AssertString(call.Argument(0), rt) name := util.AssertString(call.Argument(0), rt)
valueType := assertValueType(call.Argument(1), rt) valueType := assertValueType(call.Argument(1), rt)
hasAttr := HasAttribute(toResource(r), name, valueType) hasAttr := share.HasAttribute(toResource(r), name, valueType)
return rt.ToValue(hasAttr) return rt.ToValue(hasAttr)
} }
@ -268,7 +269,7 @@ func (r *gojaResource) Get(call goja.FunctionCall, rt *goja.Runtime) goja.Value
defaultValue = call.Argument(2).Export() defaultValue = call.Argument(2).Export()
} }
attr := GetAttribute(toResource(r), name, valueType) attr := share.GetAttribute(toResource(r), name, valueType)
if attr == nil { if attr == nil {
return rt.ToValue(defaultValue) return rt.ToValue(defaultValue)
@ -278,14 +279,14 @@ func (r *gojaResource) Get(call goja.FunctionCall, rt *goja.Runtime) goja.Value
} }
type gojaAttribute struct { type gojaAttribute struct {
Name string `goja:"name" json:"name"` Name string `goja:"name" json:"name"`
Type ValueType `goja:"type" json:"type"` Type share.ValueType `goja:"type" json:"type"`
Value any `goja:"value" json:"value"` Value any `goja:"value" json:"value"`
CreatedAt time.Time `goja:"createdAt" json:"createdAt"` CreatedAt time.Time `goja:"createdAt" json:"createdAt"`
UpdatedAt time.Time `goja:"updatedAt" json:"updatedAt"` UpdatedAt time.Time `goja:"updatedAt" json:"updatedAt"`
} }
func toGojaResource(res Resource) *gojaResource { func toGojaResource(res share.Resource) *gojaResource {
attributes := make([]*gojaAttribute, len(res.Attributes())) attributes := make([]*gojaAttribute, len(res.Attributes()))
for idx, attr := range res.Attributes() { for idx, attr := range res.Attributes() {
@ -305,7 +306,7 @@ func toGojaResource(res Resource) *gojaResource {
} }
} }
func toGojaResources(resources []Resource) []*gojaResource { func toGojaResources(resources []share.Resource) []*gojaResource {
gojaResources := make([]*gojaResource, len(resources)) gojaResources := make([]*gojaResource, len(resources))
for idx, res := range resources { for idx, res := range resources {
gojaResources[idx] = toGojaResource(res) gojaResources[idx] = toGojaResource(res)
@ -313,19 +314,19 @@ func toGojaResources(resources []Resource) []*gojaResource {
return gojaResources return gojaResources
} }
func toResource(res *gojaResource) Resource { func toResource(res *gojaResource) share.Resource {
return NewBaseResource( return share.NewBaseResource(
res.Origin, res.Origin,
res.ID, res.ID,
toAttributes(res.Attributes)..., toAttributes(res.Attributes)...,
) )
} }
func toAttributes(gojaAttributes []*gojaAttribute) []Attribute { func toAttributes(gojaAttributes []*gojaAttribute) []share.Attribute {
attributes := make([]Attribute, len(gojaAttributes)) attributes := make([]share.Attribute, len(gojaAttributes))
for idx, gojaAttr := range gojaAttributes { for idx, gojaAttr := range gojaAttributes {
attr := NewBaseAttribute( attr := share.NewBaseAttribute(
gojaAttr.Name, gojaAttr.Name,
gojaAttr.Type, gojaAttr.Type,
gojaAttr.Value, gojaAttr.Value,

View File

@ -1,21 +1,23 @@
package testsuite package share
import ( import (
"context" "context"
"io/fs" "os"
"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/module"
"forge.cadoles.com/arcad/edge/pkg/module/share" "forge.cadoles.com/arcad/edge/pkg/storage/driver"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
) )
func TestModule(t *testing.T, newRepo NewTestRepoFunc) { func TestModule(t *testing.T) {
logger.SetLevel(logger.LevelDebug) logger.SetLevel(logger.LevelDebug)
repo, err := newRepo("module") store, err := driver.NewShareStore("sqlite://testdata/test_share_module.sqlite")
if err != nil { if err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }
@ -23,10 +25,10 @@ func TestModule(t *testing.T, newRepo NewTestRepoFunc) {
server := app.NewServer( server := app.NewServer(
module.ContextModuleFactory(), module.ContextModuleFactory(),
module.ConsoleModuleFactory(), module.ConsoleModuleFactory(),
share.ModuleFactory("test.app.edge", repo), ModuleFactory("test.app.edge", store),
) )
data, err := fs.ReadFile(testData, "testdata/share.js") data, err := os.ReadFile("testdata/share.js")
if err != nil { if err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }

View File

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

View File

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

View File

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

View File

@ -2,12 +2,12 @@ package store
import ( import (
"context" "context"
"io/ioutil" "os"
"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/module"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite" "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -22,7 +22,7 @@ func TestStoreModule(t *testing.T) {
ModuleFactory(store), ModuleFactory(store),
) )
data, err := ioutil.ReadFile("testdata/store.js") data, err := os.ReadFile("testdata/store.js")
if err != nil { if err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }

View File

@ -0,0 +1,35 @@
package driver
import (
"net/url"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
var blobStoreFactories = make(map[string]BlobStoreFactory, 0)
type BlobStoreFactory func(url *url.URL) (storage.BlobStore, error)
func RegisterBlobStoreFactory(scheme string, factory BlobStoreFactory) {
blobStoreFactories[scheme] = factory
}
func NewBlobStore(dsn string) (storage.BlobStore, error) {
url, err := url.Parse(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
factory, exists := blobStoreFactories[url.Scheme]
if !exists {
return nil, errors.WithStack(ErrSchemeNotRegistered)
}
store, err := factory(url)
if err != nil {
return nil, errors.WithStack(err)
}
return store, nil
}

View File

@ -0,0 +1,35 @@
package driver
import (
"net/url"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
var documentStoreFactories = make(map[string]DocumentStoreFactory, 0)
type DocumentStoreFactory func(url *url.URL) (storage.DocumentStore, error)
func RegisterDocumentStoreFactory(scheme string, factory DocumentStoreFactory) {
documentStoreFactories[scheme] = factory
}
func NewDocumentStore(dsn string) (storage.DocumentStore, error) {
url, err := url.Parse(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
factory, exists := documentStoreFactories[url.Scheme]
if !exists {
return nil, errors.WithStack(ErrSchemeNotRegistered)
}
store, err := factory(url)
if err != nil {
return nil, errors.WithStack(err)
}
return store, nil
}

View File

@ -0,0 +1,5 @@
package driver
import "errors"
var ErrSchemeNotRegistered = errors.New("scheme was not registered")

View File

@ -0,0 +1,239 @@
package client
import (
"context"
"io"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
"github.com/pkg/errors"
)
type BlobBucket struct {
name string
id blob.BucketID
call CallFunc
}
// Size implements storage.BlobBucket
func (b *BlobBucket) Size(ctx context.Context) (int64, error) {
args := blob.GetBucketSizeArgs{
BucketID: b.id,
}
reply := blob.GetBucketSizeReply{}
if err := b.call(ctx, "Service.GetBucketSize", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
return reply.Size, nil
}
// Name implements storage.BlobBucket
func (b *BlobBucket) Name() string {
return b.name
}
// Close implements storage.BlobBucket
func (b *BlobBucket) Close() error {
args := blob.CloseBucketArgs{
BucketID: b.id,
}
reply := blob.CloseBucketReply{}
if err := b.call(context.Background(), "Service.CloseBucket", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// Delete implements storage.BlobBucket
func (b *BlobBucket) Delete(ctx context.Context, id storage.BlobID) error {
args := blob.DeleteBucketArgs{
BucketName: b.name,
}
reply := blob.DeleteBucketReply{}
if err := b.call(context.Background(), "Service.DeleteBucket", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// Get implements storage.BlobBucket
func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobInfo, error) {
args := blob.GetBlobInfoArgs{
BucketID: b.id,
BlobID: id,
}
reply := blob.GetBlobInfoReply{}
if err := b.call(context.Background(), "Service.GetBlobInfo", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.BlobInfo, nil
}
// List implements storage.BlobBucket
func (b *BlobBucket) List(ctx context.Context) ([]storage.BlobInfo, error) {
args := blob.ListBlobInfoArgs{
BucketID: b.id,
}
reply := blob.ListBlobInfoReply{}
if err := b.call(context.Background(), "Service.ListBlobInfo", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.BlobInfos, nil
}
// NewReader implements storage.BlobBucket
func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadSeekCloser, error) {
args := blob.NewBlobReaderArgs{
BucketID: b.id,
BlobID: id,
}
reply := blob.NewBlobReaderReply{}
if err := b.call(context.Background(), "Service.NewBlobReader", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return &blobReaderCloser{
readerID: reply.ReaderID,
call: b.call,
}, nil
}
// NewWriter implements storage.BlobBucket
func (b *BlobBucket) NewWriter(ctx context.Context, id storage.BlobID) (io.WriteCloser, error) {
args := blob.NewBlobWriterArgs{
BucketID: b.id,
BlobID: id,
}
reply := blob.NewBlobWriterReply{}
if err := b.call(context.Background(), "Service.NewBlobWriter", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return &blobWriterCloser{
blobID: id,
writerID: reply.WriterID,
call: b.call,
}, nil
}
type blobWriterCloser struct {
blobID storage.BlobID
writerID blob.WriterID
call CallFunc
}
// Write implements io.WriteCloser
func (bwc *blobWriterCloser) Write(data []byte) (int, error) {
args := blob.WriteBlobArgs{
WriterID: bwc.writerID,
Data: data,
}
reply := blob.WriteBlobReply{}
if err := bwc.call(context.Background(), "Service.WriteBlob", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
return reply.Written, nil
}
// Close implements io.WriteCloser
func (bwc *blobWriterCloser) Close() error {
args := blob.CloseWriterArgs{
WriterID: bwc.writerID,
}
reply := blob.CloseBucketReply{}
if err := bwc.call(context.Background(), "Service.CloseWriter", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
type blobReaderCloser struct {
readerID blob.ReaderID
call func(ctx context.Context, serviceMethod string, args any, reply any) error
}
// Read implements io.ReadSeekCloser
func (brc *blobReaderCloser) Read(p []byte) (int, error) {
args := blob.ReadBlobArgs{
ReaderID: brc.readerID,
Length: len(p),
}
reply := blob.ReadBlobReply{}
if err := brc.call(context.Background(), "Service.ReadBlob", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
copy(p, reply.Data)
if reply.EOF {
return reply.Read, io.EOF
}
return reply.Read, nil
}
// Seek implements io.ReadSeekCloser
func (brc *blobReaderCloser) Seek(offset int64, whence int) (int64, error) {
args := blob.SeekBlobArgs{
ReaderID: brc.readerID,
Offset: offset,
Whence: whence,
}
reply := blob.SeekBlobReply{}
if err := brc.call(context.Background(), "Service.SeekBlob", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
return reply.Read, nil
}
// Close implements io.ReadSeekCloser
func (brc *blobReaderCloser) Close() error {
args := blob.CloseReaderArgs{
ReaderID: brc.readerID,
}
reply := blob.CloseReaderReply{}
if err := brc.call(context.Background(), "Service.CloseReader", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
var (
_ storage.BlobBucket = &BlobBucket{}
_ storage.BlobInfo = &BlobInfo{}
_ io.WriteCloser = &blobWriterCloser{}
_ io.ReadSeekCloser = &blobReaderCloser{}
)

View File

@ -0,0 +1,40 @@
package client
import (
"time"
"forge.cadoles.com/arcad/edge/pkg/storage"
)
type BlobInfo struct {
id storage.BlobID
bucket string
contentType string
modTime time.Time
size int64
}
// Bucket implements storage.BlobInfo
func (i *BlobInfo) Bucket() string {
return i.bucket
}
// ID implements storage.BlobInfo
func (i *BlobInfo) ID() storage.BlobID {
return i.id
}
// ContentType implements storage.BlobInfo
func (i *BlobInfo) ContentType() string {
return i.contentType
}
// ModTime implements storage.BlobInfo
func (i *BlobInfo) ModTime() time.Time {
return i.modTime
}
// Size implements storage.BlobInfo
func (i *BlobInfo) Size() int64 {
return i.size
}

View File

@ -0,0 +1,101 @@
package client
import (
"context"
"net/url"
"github.com/keegancsmith/rpc"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
"github.com/pkg/errors"
)
type BlobStore struct {
serverURL *url.URL
}
// DeleteBucket implements storage.BlobStore.
func (s *BlobStore) DeleteBucket(ctx context.Context, name string) error {
args := &blob.DeleteBucketArgs{
BucketName: name,
}
if err := s.call(ctx, "Service.DeleteBucket", args, nil); err != nil {
return errors.WithStack(err)
}
return nil
}
// ListBuckets implements storage.BlobStore.
func (s *BlobStore) ListBuckets(ctx context.Context) ([]string, error) {
args := &blob.ListBucketsArgs{}
reply := blob.ListBucketsReply{}
if err := s.call(ctx, "Service.ListBuckets", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Buckets, nil
}
// OpenBucket implements storage.BlobStore.
func (s *BlobStore) OpenBucket(ctx context.Context, name string) (storage.BlobBucket, error) {
args := &blob.OpenBucketArgs{
BucketName: name,
}
reply := &blob.OpenBucketReply{}
if err := s.call(ctx, "Service.OpenBucket", args, reply); err != nil {
return nil, errors.WithStack(err)
}
return &BlobBucket{
name: name,
id: reply.BucketID,
call: s.call,
}, nil
}
func (s *BlobStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *BlobStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
}
}()
if err := fn(ctx, client); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewBlobStore(serverURL *url.URL) *BlobStore {
return &BlobStore{serverURL}
}
var _ storage.BlobStore = &BlobStore{}

View File

@ -0,0 +1,87 @@
package client
import (
"context"
"fmt"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestBlobStore(t *testing.T) {
t.Parallel()
if testing.Verbose() {
logger.SetLevel(logger.LevelDebug)
}
httpServer, err := startNewBlobStoreServer()
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer httpServer.Close()
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
store := NewBlobStore(serverURL)
testsuite.TestBlobStore(context.Background(), t, store)
}
func BenchmarkBlobStore(t *testing.B) {
logger.SetLevel(logger.LevelError)
httpServer, err := startNewBlobStoreServer()
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer httpServer.Close()
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
store := NewBlobStore(serverURL)
testsuite.BenchmarkBlobStore(t, store)
}
func getSQLiteBlobStore() (*sqlite.BlobStore, error) {
file := "./testdata/blobstore_test.sqlite"
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := sqlite.NewBlobStore(dsn)
return store, nil
}
func startNewBlobStoreServer() (*httptest.Server, error) {
store, err := getSQLiteBlobStore()
if err != nil {
return nil, errors.WithStack(err)
}
server := server.NewBlobStoreServer(store)
httpServer := httptest.NewServer(server)
return httpServer, nil
}

View File

@ -0,0 +1,134 @@
package client
import (
"context"
"net/url"
"github.com/keegancsmith/rpc"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/document"
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
)
type DocumentStore struct {
serverURL *url.URL
}
// Delete implements storage.DocumentStore.
func (s *DocumentStore) Delete(ctx context.Context, collection string, id storage.DocumentID) error {
args := document.DeleteDocumentArgs{
Collection: collection,
DocumentID: id,
}
reply := document.DeleteDocumentReply{}
if err := s.call(ctx, "Service.DeleteDocument", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// Get implements storage.DocumentStore.
func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.DocumentID) (storage.Document, error) {
args := document.GetDocumentArgs{
Collection: collection,
DocumentID: id,
}
reply := document.GetDocumentReply{}
if err := s.call(ctx, "Service.GetDocument", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Document, nil
}
// Query implements storage.DocumentStore.
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)
}
args := document.QueryDocumentsArgs{
Collection: collection,
Filter: nil,
Options: opts,
}
if filter != nil {
args.Filter = filter.AsMap()
}
reply := document.QueryDocumentsReply{
Documents: []storage.Document{},
}
if err := s.call(ctx, "Service.QueryDocuments", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Documents, nil
}
// Upsert implements storage.DocumentStore.
func (s *DocumentStore) Upsert(ctx context.Context, collection string, doc storage.Document) (storage.Document, error) {
args := document.UpsertDocumentArgs{
Collection: collection,
Document: doc,
}
reply := document.UpsertDocumentReply{}
if err := s.call(ctx, "Service.UpsertDocument", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Document, nil
}
func (s *DocumentStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *DocumentStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
}
}()
if err := fn(ctx, client); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewDocumentStore(url *url.URL) *DocumentStore {
return &DocumentStore{url}
}
var _ storage.DocumentStore = &DocumentStore{}

View File

@ -0,0 +1,67 @@
package client
import (
"context"
"fmt"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestDocumentStore(t *testing.T) {
t.Parallel()
if testing.Verbose() {
logger.SetLevel(logger.LevelDebug)
}
httpServer, err := startNewDocumentStoreServer()
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer httpServer.Close()
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
store := NewDocumentStore(serverURL)
testsuite.TestDocumentStore(context.Background(), t, store)
}
func getSQLiteDocumentStore() (*sqlite.DocumentStore, error) {
file := "./testdata/documentstore_test.sqlite"
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := sqlite.NewDocumentStore(dsn)
return store, nil
}
func startNewDocumentStoreServer() (*httptest.Server, error) {
store, err := getSQLiteDocumentStore()
if err != nil {
return nil, errors.WithStack(err)
}
server := server.NewDocumentStoreServer(store)
httpServer := httptest.NewServer(server)
return httpServer, nil
}

View File

@ -0,0 +1,17 @@
package client
import (
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
func remapShareError(err error) error {
switch errors.Cause(err).Error() {
case share.ErrAttributeRequired.Error():
return share.ErrAttributeRequired
case share.ErrNotFound.Error():
return share.ErrNotFound
default:
return err
}
}

View File

@ -0,0 +1,9 @@
package client
import (
"context"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
)
type CallFunc func(ctx context.Context, serviceMethod string, args any, reply any) error

View File

@ -0,0 +1,150 @@
package client
import (
"context"
"net/url"
"forge.cadoles.com/arcad/edge/pkg/app"
server "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/share"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/keegancsmith/rpc"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type ShareStore struct {
serverURL *url.URL
}
// DeleteAttributes implements share.Store.
func (s *ShareStore) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error {
args := server.DeleteAttributesArgs{
Origin: origin,
ResourceID: resourceID,
Names: names,
}
reply := server.DeleteAttributesArgs{}
if err := s.call(ctx, "Service.DeleteAttributes", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// DeleteResource implements share.Store.
func (s *ShareStore) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error {
args := server.DeleteResourceArgs{
Origin: origin,
ResourceID: resourceID,
}
reply := server.DeleteResourceReply{}
if err := s.call(ctx, "Service.DeleteResource", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// FindResources implements share.Store.
func (s *ShareStore) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) {
options := share.NewFindResourcesOptions(funcs...)
args := server.FindResourcesArgs{
Options: options,
}
reply := server.FindResourcesReply{}
if err := s.call(ctx, "Service.FindResources", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
resources := make([]share.Resource, len(reply.Resources))
for idx, res := range reply.Resources {
resources[idx] = res
}
return resources, nil
}
// GetResource implements share.Store.
func (s *ShareStore) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) {
args := server.GetResourceArgs{
Origin: origin,
ResourceID: resourceID,
}
reply := server.GetResourceReply{}
if err := s.call(ctx, "Service.GetResource", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Resource, nil
}
// UpdateAttributes implements share.Store.
func (s *ShareStore) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) {
serializableAttributes := make([]*server.SerializableAttribute, len(attributes))
for attrIdx, attr := range attributes {
serializableAttributes[attrIdx] = server.FromAttribute(attr)
}
args := server.UpdateAttributesArgs{
Origin: origin,
ResourceID: resourceID,
Attributes: serializableAttributes,
}
reply := server.UpdateAttributesReply{}
if err := s.call(ctx, "Service.UpdateAttributes", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Resource, nil
}
func (s *ShareStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
return errors.WithStack(remapShareError(err))
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *ShareStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
}
}()
if err := fn(ctx, client); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewShareStore(url *url.URL) *ShareStore {
return &ShareStore{url}
}
var _ share.Store = &ShareStore{}

View File

@ -0,0 +1,67 @@
package client
import (
"fmt"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"forge.cadoles.com/arcad/edge/pkg/storage/share/testsuite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestShareStore(t *testing.T) {
t.Parallel()
if testing.Verbose() {
logger.SetLevel(logger.LevelDebug)
}
testsuite.TestStore(t, func(testName string) (share.Store, error) {
httpServer, err := startNewShareStoreServer(testName)
if err != nil {
return nil, errors.WithStack(err)
}
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
return NewShareStore(serverURL), nil
})
}
func getSQLiteShareStore(testName string) (*sqlite.ShareStore, error) {
filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
file := fmt.Sprintf("./testdata/sharestore_test_%s.sqlite", filename)
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := sqlite.NewShareStore(dsn)
return store, nil
}
func startNewShareStoreServer(testName string) (*httptest.Server, error) {
store, err := getSQLiteShareStore(testName)
if err != nil {
return nil, errors.WithStack(err)
}
server := server.NewShareStoreServer(store)
httpServer := httptest.NewServer(server)
return httpServer, nil
}

View File

@ -0,0 +1,22 @@
package rpc
import (
"net/url"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/client"
)
func init() {
driver.RegisterDocumentStoreFactory("rpc", documentStoreFactory)
driver.RegisterBlobStoreFactory("rpc", blobStoreFactory)
}
func documentStoreFactory(url *url.URL) (storage.DocumentStore, error) {
return client.NewDocumentStore(url), nil
}
func blobStoreFactory(url *url.URL) (storage.BlobStore, error) {
return client.NewBlobStore(url), nil
}

View File

@ -0,0 +1,42 @@
package gob
import (
"time"
"forge.cadoles.com/arcad/edge/pkg/storage"
)
type BlobInfo struct {
Bucket_ string
ContentType_ string
BlobID_ storage.BlobID
ModTime_ time.Time
Size_ int64
}
// Bucket implements storage.BlobInfo.
func (bi *BlobInfo) Bucket() string {
return bi.Bucket_
}
// ContentType implements storage.BlobInfo.
func (bi *BlobInfo) ContentType() string {
return bi.ContentType_
}
// ID implements storage.BlobInfo.
func (bi *BlobInfo) ID() storage.BlobID {
return bi.BlobID_
}
// ModTime implements storage.BlobInfo.
func (bi *BlobInfo) ModTime() time.Time {
return bi.ModTime_
}
// Size implements storage.BlobInfo.
func (bi *BlobInfo) Size() int64 {
return bi.Size_
}
var _ storage.BlobInfo = &BlobInfo{}

View File

@ -0,0 +1,18 @@
package gob
import (
"encoding/gob"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage"
)
func init() {
gob.Register(storage.Document{})
gob.Register(storage.DocumentID(""))
gob.Register(time.Time{})
gob.Register(map[string]interface{}{})
gob.Register([]interface{}{})
gob.Register([]map[string]interface{}{})
gob.Register(&BlobInfo{})
}

View File

@ -0,0 +1,31 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type CloseBucketArgs struct {
BucketID BucketID
}
type CloseBucketReply struct {
}
func (s *Service) CloseBucket(ctx context.Context, args *CloseBucketArgs, reply *CloseBucketReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
if err := bucket.Close(); err != nil {
return errors.WithStack(err)
}
s.buckets.Delete(args.BucketID)
*reply = CloseBucketReply{}
return nil
}

View File

@ -0,0 +1,31 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type CloseReaderArgs struct {
ReaderID ReaderID
}
type CloseReaderReply struct {
}
func (s *Service) CloseReader(ctx context.Context, args *CloseReaderArgs, reply *CloseReaderReply) error {
reader, err := s.getOpenedReader(args.ReaderID)
if err != nil {
return errors.WithStack(err)
}
if err := reader.Close(); err != nil {
return errors.WithStack(err)
}
s.readers.Delete(args.ReaderID)
*reply = CloseReaderReply{}
return nil
}

View File

@ -0,0 +1,31 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type CloseWriterArgs struct {
WriterID WriterID
}
type CloseWriterReply struct {
}
func (s *Service) CloseWriter(ctx context.Context, args *CloseWriterArgs, reply *CloseWriterReply) error {
writer, err := s.getOpenedWriter(args.WriterID)
if err != nil {
return errors.WithStack(err)
}
if err := writer.Close(); err != nil {
return errors.WithStack(err)
}
s.writers.Delete(args.WriterID)
*reply = CloseWriterReply{}
return nil
}

View File

@ -0,0 +1,22 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type DeleteBucketArgs struct {
BucketName string
}
type DeleteBucketReply struct {
}
func (s *Service) DeleteBucket(ctx context.Context, args *DeleteBucketArgs, reply *DeleteBucketReply) error {
if err := s.store.DeleteBucket(ctx, args.BucketName); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@ -0,0 +1,42 @@
package blob
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
"github.com/pkg/errors"
)
type GetBlobInfoArgs struct {
BlobID storage.BlobID
BucketID BucketID
}
type GetBlobInfoReply struct {
BlobInfo storage.BlobInfo
}
func (s *Service) GetBlobInfo(ctx context.Context, args *GetBlobInfoArgs, reply *GetBlobInfoReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
blobInfo, err := bucket.Get(ctx, args.BlobID)
if err != nil {
return errors.WithStack(err)
}
*reply = GetBlobInfoReply{
BlobInfo: &gob.BlobInfo{
Bucket_: blobInfo.Bucket(),
ContentType_: blobInfo.ContentType(),
BlobID_: blobInfo.ID(),
ModTime_: blobInfo.ModTime(),
Size_: blobInfo.Size(),
},
}
return nil
}

View File

@ -0,0 +1,33 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type GetBucketSizeArgs struct {
BucketID BucketID
}
type GetBucketSizeReply struct {
Size int64
}
func (s *Service) GetBucketSize(ctx context.Context, args *GetBucketSizeArgs, reply *GetBucketSizeReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
size, err := bucket.Size(ctx)
if err != nil {
return errors.WithStack(err)
}
*reply = GetBucketSizeReply{
Size: size,
}
return nil
}

View File

@ -0,0 +1,34 @@
package blob
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type ListBlobInfoArgs struct {
BucketID BucketID
}
type ListBlobInfoReply struct {
BlobInfos []storage.BlobInfo
}
func (s *Service) ListBlobInfo(ctx context.Context, args *ListBlobInfoArgs, reply *ListBlobInfoReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
blobInfos, err := bucket.List(ctx)
if err != nil {
return errors.WithStack(err)
}
*reply = ListBlobInfoReply{
BlobInfos: blobInfos,
}
return nil
}

View File

@ -0,0 +1,27 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type ListBucketsArgs struct {
}
type ListBucketsReply struct {
Buckets []string
}
func (s *Service) ListBuckets(ctx context.Context, args *ListBucketsArgs, reply *ListBucketsReply) error {
buckets, err := s.store.ListBuckets(ctx)
if err != nil {
return errors.WithStack(err)
}
*reply = ListBucketsReply{
Buckets: buckets,
}
return nil
}

View File

@ -0,0 +1,57 @@
package blob
import (
"context"
"io"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type NewBlobReaderArgs struct {
BlobID storage.BlobID
BucketID BucketID
}
type NewBlobReaderReply struct {
ReaderID ReaderID
}
func (s *Service) NewBlobReader(ctx context.Context, args *NewBlobReaderArgs, reply *NewBlobReaderReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
readerID, err := NewReaderID()
if err != nil {
return errors.WithStack(err)
}
reader, err := bucket.NewReader(ctx, args.BlobID)
if err != nil {
return errors.WithStack(err)
}
s.readers.Store(readerID, reader)
*reply = NewBlobReaderReply{
ReaderID: readerID,
}
return nil
}
func (s *Service) getOpenedReader(id ReaderID) (io.ReadSeekCloser, error) {
raw, exists := s.readers.Load(id)
if !exists {
return nil, errors.Errorf("could not find writer '%s'", id)
}
reader, ok := raw.(io.ReadSeekCloser)
if !ok {
return nil, errors.Errorf("unexpected type '%T' for writer", raw)
}
return reader, nil
}

View File

@ -0,0 +1,57 @@
package blob
import (
"context"
"io"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type NewBlobWriterArgs struct {
BlobID storage.BlobID
BucketID BucketID
}
type NewBlobWriterReply struct {
WriterID WriterID
}
func (s *Service) NewBlobWriter(ctx context.Context, args *NewBlobWriterArgs, reply *NewBlobWriterReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
writerID, err := NewWriterID()
if err != nil {
return errors.WithStack(err)
}
writer, err := bucket.NewWriter(ctx, args.BlobID)
if err != nil {
return errors.WithStack(err)
}
s.writers.Store(writerID, writer)
*reply = NewBlobWriterReply{
WriterID: writerID,
}
return nil
}
func (s *Service) getOpenedWriter(id WriterID) (io.WriteCloser, error) {
raw, exists := s.writers.Load(id)
if !exists {
return nil, errors.Errorf("could not find writer '%s'", id)
}
writer, ok := raw.(io.WriteCloser)
if !ok {
return nil, errors.Errorf("unexpected type '%T' for writer", raw)
}
return writer, nil
}

View File

@ -0,0 +1,50 @@
package blob
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type OpenBucketArgs struct {
BucketName string
}
type OpenBucketReply struct {
BucketID BucketID
}
func (s *Service) OpenBucket(ctx context.Context, args *OpenBucketArgs, reply *OpenBucketReply) error {
bucket, err := s.store.OpenBucket(ctx, args.BucketName)
if err != nil {
return errors.WithStack(err)
}
bucketID, err := NewBucketID()
if err != nil {
return errors.WithStack(err)
}
s.buckets.Store(bucketID, bucket)
*reply = OpenBucketReply{
BucketID: bucketID,
}
return nil
}
func (s *Service) getOpenedBucket(id BucketID) (storage.BlobBucket, error) {
raw, exists := s.buckets.Load(id)
if !exists {
return nil, errors.WithStack(storage.ErrBucketClosed)
}
bucket, ok := raw.(storage.BlobBucket)
if !ok {
return nil, errors.Errorf("unexpected type '%T' for blob bucket", raw)
}
return bucket, nil
}

View File

@ -0,0 +1,41 @@
package blob
import (
"context"
"io"
"github.com/pkg/errors"
)
type ReadBlobArgs struct {
ReaderID ReaderID
Length int
}
type ReadBlobReply struct {
Data []byte
Read int
EOF bool
}
func (s *Service) ReadBlob(ctx context.Context, args *ReadBlobArgs, reply *ReadBlobReply) error {
reader, err := s.getOpenedReader(args.ReaderID)
if err != nil {
return errors.WithStack(err)
}
buff := make([]byte, args.Length)
read, err := reader.Read(buff)
if err != nil && !errors.Is(err, io.EOF) {
return errors.WithStack(err)
}
*reply = ReadBlobReply{
Read: read,
Data: buff,
EOF: errors.Is(err, io.EOF),
}
return nil
}

View File

@ -0,0 +1,38 @@
package blob
import (
"context"
"io"
"github.com/pkg/errors"
)
type SeekBlobArgs struct {
ReaderID ReaderID
Offset int64
Whence int
}
type SeekBlobReply struct {
Read int64
EOF bool
}
func (s *Service) SeekBlob(ctx context.Context, args *SeekBlobArgs, reply *SeekBlobReply) error {
reader, err := s.getOpenedReader(args.ReaderID)
if err != nil {
return errors.WithStack(err)
}
read, err := reader.Seek(args.Offset, args.Whence)
if err != nil && !errors.Is(err, io.EOF) {
return errors.WithStack(err)
}
*reply = SeekBlobReply{
Read: read,
EOF: errors.Is(err, io.EOF),
}
return nil
}

View File

@ -0,0 +1,60 @@
package blob
import (
"fmt"
"sync"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/google/uuid"
"github.com/pkg/errors"
)
type BucketID string
type WriterID string
type ReaderID string
type Service struct {
store storage.BlobStore
buckets sync.Map
writers sync.Map
readers sync.Map
}
func NewService(store storage.BlobStore) *Service {
return &Service{
store: store,
}
}
func NewBucketID() (BucketID, error) {
uuid, err := uuid.NewUUID()
if err != nil {
return "", errors.WithStack(err)
}
id := BucketID(fmt.Sprintf("bucket-%s", uuid.String()))
return id, nil
}
func NewWriterID() (WriterID, error) {
uuid, err := uuid.NewUUID()
if err != nil {
return "", errors.WithStack(err)
}
id := WriterID(fmt.Sprintf("writer-%s", uuid.String()))
return id, nil
}
func NewReaderID() (ReaderID, error) {
uuid, err := uuid.NewUUID()
if err != nil {
return "", errors.WithStack(err)
}
id := ReaderID(fmt.Sprintf("reader-%s", uuid.String()))
return id, nil
}

View File

@ -0,0 +1,34 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type WriteBlobArgs struct {
WriterID WriterID
Data []byte
}
type WriteBlobReply struct {
Written int
}
func (s *Service) WriteBlob(ctx context.Context, args *WriteBlobArgs, reply *WriteBlobReply) error {
writer, err := s.getOpenedWriter(args.WriterID)
if err != nil {
return errors.WithStack(err)
}
written, err := writer.Write(args.Data)
if err != nil {
return errors.WithStack(err)
}
*reply = WriteBlobReply{
Written: written,
}
return nil
}

View File

@ -0,0 +1,26 @@
package document
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type DeleteDocumentArgs struct {
Collection string
DocumentID storage.DocumentID
}
type DeleteDocumentReply struct {
}
func (s *Service) DeleteDocument(ctx context.Context, args DeleteDocumentArgs, reply *DeleteDocumentReply) error {
if err := s.store.Delete(ctx, args.Collection, args.DocumentID); err != nil {
return errors.WithStack(err)
}
*reply = DeleteDocumentReply{}
return nil
}

View File

@ -0,0 +1,30 @@
package document
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type GetDocumentArgs struct {
Collection string
DocumentID storage.DocumentID
}
type GetDocumentReply struct {
Document storage.Document
}
func (s *Service) GetDocument(ctx context.Context, args GetDocumentArgs, reply *GetDocumentReply) error {
document, err := s.store.Get(ctx, args.Collection, args.DocumentID)
if err != nil {
return errors.WithStack(err)
}
*reply = GetDocumentReply{
Document: document,
}
return nil
}

View File

@ -0,0 +1,53 @@
package document
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
"github.com/pkg/errors"
)
type QueryDocumentsArgs struct {
Collection string
Filter map[string]any
Options *storage.QueryOptions
}
type QueryDocumentsReply struct {
Documents []storage.Document
}
func (s *Service) QueryDocuments(ctx context.Context, args QueryDocumentsArgs, reply *QueryDocumentsReply) error {
var (
argsFilter *filter.Filter
err error
)
if args.Filter != nil {
argsFilter, err = filter.NewFrom(args.Filter)
if err != nil {
return errors.WithStack(err)
}
}
documents, err := s.store.Query(ctx, args.Collection, argsFilter, withQueryOptions(args.Options))
if err != nil {
return errors.WithStack(err)
}
*reply = QueryDocumentsReply{
Documents: documents,
}
return nil
}
func withQueryOptions(opts *storage.QueryOptions) storage.QueryOptionFunc {
return func(o *storage.QueryOptions) {
o.Limit = opts.Limit
o.Offset = opts.Offset
o.OrderBy = opts.OrderBy
o.OrderDirection = opts.OrderDirection
}
}

View File

@ -0,0 +1,11 @@
package document
import "forge.cadoles.com/arcad/edge/pkg/storage"
type Service struct {
store storage.DocumentStore
}
func NewService(store storage.DocumentStore) *Service {
return &Service{store}
}

View File

@ -0,0 +1,30 @@
package document
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type UpsertDocumentArgs struct {
Collection string
Document storage.Document
}
type UpsertDocumentReply struct {
Document storage.Document
}
func (s *Service) UpsertDocument(ctx context.Context, args UpsertDocumentArgs, reply *UpsertDocumentReply) error {
document, err := s.store.Upsert(ctx, args.Collection, args.Document)
if err != nil {
return errors.WithStack(err)
}
*reply = UpsertDocumentReply{
Document: document,
}
return nil
}

View File

@ -0,0 +1,5 @@
package server
import (
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
)

View File

@ -0,0 +1,29 @@
package server
import (
"github.com/keegancsmith/rpc"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/document"
shareService "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/share"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
)
func NewBlobStoreServer(store storage.BlobStore) *rpc.Server {
server := rpc.NewServer()
server.Register(blob.NewService(store))
return server
}
func NewDocumentStoreServer(store storage.DocumentStore) *rpc.Server {
server := rpc.NewServer()
server.Register(document.NewService(store))
return server
}
func NewShareStoreServer(store share.Store) *rpc.Server {
server := rpc.NewServer()
server.Register(shareService.NewService(store))
return server
}

View File

@ -0,0 +1,28 @@
package share
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
type DeleteAttributesArgs struct {
Origin app.ID
ResourceID share.ResourceID
Names []string
}
type DeleteAttributesReply struct {
}
func (s *Service) DeleteAttributes(ctx context.Context, args DeleteAttributesArgs, reply *DeleteAttributesReply) error {
if err := s.store.DeleteAttributes(ctx, args.Origin, args.ResourceID, args.Names...); err != nil {
return errors.WithStack(err)
}
*reply = DeleteAttributesReply{}
return nil
}

View File

@ -0,0 +1,27 @@
package share
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
type DeleteResourceArgs struct {
Origin app.ID
ResourceID share.ResourceID
}
type DeleteResourceReply struct {
}
func (s *Service) DeleteResource(ctx context.Context, args DeleteResourceArgs, reply *DeleteResourceReply) error {
if err := s.store.DeleteResource(ctx, args.Origin, args.ResourceID); err != nil {
return errors.WithStack(err)
}
*reply = DeleteResourceReply{}
return nil
}

View File

@ -0,0 +1,41 @@
package share
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
type FindResourcesArgs struct {
Options *share.FindResourcesOptions
}
type FindResourcesReply struct {
Resources []*SerializableResource
}
func (s *Service) FindResources(ctx context.Context, args FindResourcesArgs, reply *FindResourcesReply) error {
resources, err := s.store.FindResources(ctx, withFindResourcesOptions(args.Options))
if err != nil {
return errors.WithStack(err)
}
serializableResources := make([]*SerializableResource, len(resources))
for resIdx, r := range resources {
serializableResources[resIdx] = FromResource(r)
}
*reply = FindResourcesReply{
Resources: serializableResources,
}
return nil
}
func withFindResourcesOptions(opts *share.FindResourcesOptions) share.FindResourcesOptionFunc {
return func(o *share.FindResourcesOptions) {
o.Name = opts.Name
o.ValueType = opts.ValueType
}
}

View File

@ -0,0 +1,31 @@
package share
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
type GetResourceArgs struct {
Origin app.ID
ResourceID share.ResourceID
}
type GetResourceReply struct {
Resource *SerializableResource
}
func (s *Service) GetResource(ctx context.Context, args GetResourceArgs, reply *GetResourceReply) error {
resource, err := s.store.GetResource(ctx, args.Origin, args.ResourceID)
if err != nil {
return errors.WithStack(err)
}
*reply = GetResourceReply{
Resource: FromResource(resource),
}
return nil
}

View File

@ -0,0 +1,94 @@
package share
import (
"time"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
)
func FromResource(res share.Resource) *SerializableResource {
serializableAttributes := make([]*SerializableAttribute, len(res.Attributes()))
for attrIdx, attr := range res.Attributes() {
serializableAttributes[attrIdx] = FromAttribute(attr)
}
return &SerializableResource{
ID_: res.ID(),
Origin_: res.Origin(),
Attributes_: serializableAttributes,
}
}
func FromAttribute(attr share.Attribute) *SerializableAttribute {
return &SerializableAttribute{
Name_: attr.Name(),
Value_: attr.Value(),
Type_: attr.Type(),
UpdatedAt_: attr.UpdatedAt(),
CreatedAt_: attr.CreatedAt(),
}
}
type SerializableResource struct {
ID_ share.ResourceID
Origin_ app.ID
Attributes_ []*SerializableAttribute
}
// Attributes implements share.Resource.
func (r *SerializableResource) Attributes() []share.Attribute {
attributes := make([]share.Attribute, len(r.Attributes_))
for idx, attr := range r.Attributes_ {
attributes[idx] = attr
}
return attributes
}
// ID implements share.Resource.
func (r *SerializableResource) ID() share.ResourceID {
return r.ID_
}
// Origin implements share.Resource.
func (r *SerializableResource) Origin() app.ID {
return r.Origin_
}
var _ share.Resource = &SerializableResource{}
type SerializableAttribute struct {
Name_ string
Value_ any
Type_ share.ValueType
UpdatedAt_ time.Time
CreatedAt_ time.Time
}
// CreatedAt implements share.Attribute.
func (a *SerializableAttribute) CreatedAt() time.Time {
return a.CreatedAt_
}
// Name implements share.Attribute.
func (a *SerializableAttribute) Name() string {
return a.Name_
}
// Type implements share.Attribute.
func (a *SerializableAttribute) Type() share.ValueType {
return a.Type_
}
// UpdatedAt implements share.Attribute.
func (a *SerializableAttribute) UpdatedAt() time.Time {
return a.UpdatedAt_
}
// Value implements share.Attribute.
func (a *SerializableAttribute) Value() any {
return a.Value_
}
var _ share.Attribute = &SerializableAttribute{}

View File

@ -0,0 +1,13 @@
package share
import (
"forge.cadoles.com/arcad/edge/pkg/storage/share"
)
type Service struct {
store share.Store
}
func NewService(store share.Store) *Service {
return &Service{store}
}

View File

@ -0,0 +1,37 @@
package share
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
type UpdateAttributesArgs struct {
Origin app.ID
ResourceID share.ResourceID
Attributes []*SerializableAttribute
}
type UpdateAttributesReply struct {
Resource *SerializableResource
}
func (s *Service) UpdateAttributes(ctx context.Context, args UpdateAttributesArgs, reply *UpdateAttributesReply) error {
attributes := make([]share.Attribute, len(args.Attributes))
for idx, attr := range args.Attributes {
attributes[idx] = attr
}
resource, err := s.store.UpdateAttributes(ctx, args.Origin, args.ResourceID, attributes...)
if err != nil {
return errors.WithStack(err)
}
*reply = UpdateAttributesReply{
Resource: FromResource(resource),
}
return nil
}

View File

@ -0,0 +1,35 @@
package driver
import (
"net/url"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
var shareStoreFactories = make(map[string]ShareStoreFactory, 0)
type ShareStoreFactory func(url *url.URL) (share.Store, error)
func RegisterShareStoreFactory(scheme string, factory ShareStoreFactory) {
shareStoreFactories[scheme] = factory
}
func NewShareStore(dsn string) (share.Store, error) {
url, err := url.Parse(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
factory, exists := shareStoreFactories[url.Scheme]
if !exists {
return nil, errors.WithStack(ErrSchemeNotRegistered)
}
store, err := factory(url)
if err != nil {
return nil, errors.WithStack(err)
}
return store, nil
}

View File

@ -0,0 +1,46 @@
package sqlite
import (
"context"
"fmt"
"os"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestBlobStore(t *testing.T) {
t.Parallel()
if testing.Verbose() {
logger.SetLevel(logger.LevelDebug)
}
file := "./testdata/blobstore_test.sqlite"
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
t.Fatalf("%+v", errors.WithStack(err))
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := NewBlobStore(dsn)
testsuite.TestBlobStore(context.Background(), t, store)
}
func BenchmarkBlobStore(t *testing.B) {
logger.SetLevel(logger.LevelError)
file := "./testdata/blobstore_test.sqlite"
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
t.Fatalf("%+v", errors.WithStack(err))
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := NewBlobStore(dsn)
testsuite.BenchmarkBlobStore(t, store)
}

View File

@ -276,7 +276,7 @@ func (s *DocumentStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) e
return nil return nil
} }
func ensureTables(ctx context.Context, db *sql.DB) error { func ensureDocumentTables(ctx context.Context, db *sql.DB) error {
err := WithTx(ctx, db, func(tx *sql.Tx) error { err := WithTx(ctx, db, func(tx *sql.Tx) error {
query := ` query := `
CREATE TABLE IF NOT EXISTS documents ( CREATE TABLE IF NOT EXISTS documents (
@ -344,7 +344,7 @@ func withLimitOffsetClause(query string, args []any, limit int, offset int) (str
} }
func NewDocumentStore(path string) *DocumentStore { func NewDocumentStore(path string) *DocumentStore {
getDB := NewGetDBFunc(path, ensureTables) getDB := NewGetDBFunc(path, ensureDocumentTables)
return &DocumentStore{ return &DocumentStore{
getDB: getDB, getDB: getDB,
@ -352,7 +352,7 @@ func NewDocumentStore(path string) *DocumentStore {
} }
func NewDocumentStoreWithDB(db *sql.DB) *DocumentStore { func NewDocumentStoreWithDB(db *sql.DB) *DocumentStore {
getDB := NewGetDBFuncFromDB(db, ensureTables) getDB := NewGetDBFuncFromDB(db, ensureDocumentTables)
return &DocumentStore{ return &DocumentStore{
getDB: getDB, getDB: getDB,

View File

@ -1,6 +1,7 @@
package sqlite package sqlite
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"testing" "testing"
@ -24,5 +25,5 @@ func TestDocumentStore(t *testing.T) {
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds()) dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := NewDocumentStore(dsn) store := NewDocumentStore(dsn)
testsuite.TestDocumentStore(t, store) testsuite.TestDocumentStore(context.Background(), t, store)
} }

View File

@ -0,0 +1,75 @@
package sqlite
import (
"net/url"
"os"
"path/filepath"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
func init() {
driver.RegisterDocumentStoreFactory("sqlite", documentStoreFactory)
driver.RegisterBlobStoreFactory("sqlite", blobStoreFactory)
driver.RegisterShareStoreFactory("sqlite", shareStoreFactory)
}
func documentStoreFactory(url *url.URL) (storage.DocumentStore, error) {
dir := filepath.Dir(url.Host + url.Path)
if dir != ":memory:" {
if err := os.MkdirAll(dir, os.FileMode(0750)); err != nil {
return nil, errors.WithStack(err)
}
}
path := url.Host + url.Path + "?" + url.RawQuery
db, err := Open(path)
if err != nil {
return nil, errors.WithStack(err)
}
return NewDocumentStoreWithDB(db), nil
}
func blobStoreFactory(url *url.URL) (storage.BlobStore, error) {
dir := filepath.Dir(url.Host + url.Path)
if dir != ":memory:" {
if err := os.MkdirAll(dir, os.FileMode(0750)); err != nil {
return nil, errors.WithStack(err)
}
}
path := url.Host + url.Path + "?" + url.RawQuery
db, err := Open(path)
if err != nil {
return nil, errors.WithStack(err)
}
return NewBlobStoreWithDB(db), nil
}
func shareStoreFactory(url *url.URL) (share.Store, error) {
dir := filepath.Dir(url.Host + url.Path)
if dir != ":memory:" {
if err := os.MkdirAll(dir, os.FileMode(0750)); err != nil {
return nil, errors.WithStack(err)
}
}
path := url.Host + url.Path + "?" + url.RawQuery
db, err := Open(path)
if err != nil {
return nil, errors.WithStack(err)
}
return NewShareStoreWithDB(db), nil
}

View File

@ -7,19 +7,18 @@ import (
"time" "time"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module/share" "forge.cadoles.com/arcad/edge/pkg/storage/share"
"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"
) )
type Repository struct { type ShareStore struct {
getDB sqlite.GetDBFunc getDB GetDBFunc
} }
// DeleteAttributes implements share.Repository // DeleteAttributes implements share.Repository
func (r *Repository) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error { func (s *ShareStore) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error {
err := r.withTx(ctx, func(tx *sql.Tx) error { err := s.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
DELETE FROM resources DELETE FROM resources
WHERE origin = $1 AND resource_id = $2 WHERE origin = $1 AND resource_id = $2
@ -76,8 +75,8 @@ func (r *Repository) DeleteAttributes(ctx context.Context, origin app.ID, resour
} }
// DeleteResource implements share.Repository // DeleteResource implements share.Repository
func (r *Repository) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error { func (s *ShareStore) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error {
err := r.withTx(ctx, func(tx *sql.Tx) error { err := s.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
DELETE FROM resources DELETE FROM resources
WHERE origin = $1 AND resource_id = $2 WHERE origin = $1 AND resource_id = $2
@ -115,12 +114,12 @@ func (r *Repository) DeleteResource(ctx context.Context, origin app.ID, resource
} }
// FindResources implements share.Repository // FindResources implements share.Repository
func (r *Repository) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) { func (s *ShareStore) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) {
opts := share.FillFindResourcesOptions(funcs...) opts := share.NewFindResourcesOptions(funcs...)
var resources []share.Resource var resources []share.Resource
err := r.withTx(ctx, func(tx *sql.Tx) error { err := s.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
SELECT SELECT
main.origin, main.resource_id, main.origin, main.resource_id,
@ -222,14 +221,14 @@ func (r *Repository) FindResources(ctx context.Context, funcs ...share.FindResou
} }
// GetResource implements share.Repository // GetResource implements share.Repository
func (r *Repository) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) { func (s *ShareStore) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) {
var ( var (
resource *share.BaseResource resource *share.BaseResource
err error err error
) )
err = r.withTx(ctx, func(tx *sql.Tx) error { err = s.withTx(ctx, func(tx *sql.Tx) error {
resource, err = r.getResourceWithinTx(ctx, tx, origin, resourceID) resource, err = s.getResourceWithinTx(ctx, tx, origin, resourceID)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -244,13 +243,13 @@ func (r *Repository) GetResource(ctx context.Context, origin app.ID, resourceID
} }
// UpdateAttributes implements share.Repository // UpdateAttributes implements share.Repository
func (r *Repository) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) { func (s *ShareStore) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) {
if len(attributes) == 0 { if len(attributes) == 0 {
return nil, errors.WithStack(share.ErrAttributeRequired) return nil, errors.WithStack(share.ErrAttributeRequired)
} }
var resource *share.BaseResource var resource *share.BaseResource
err := r.withTx(ctx, func(tx *sql.Tx) error { err := s.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
INSERT INTO resources (origin, resource_id, name, type, value, created_at, updated_at) INSERT INTO resources (origin, resource_id, name, type, value, created_at, updated_at)
VALUES($1, $2, $3, $4, $5, $6, $6) VALUES($1, $2, $3, $4, $5, $6, $6)
@ -289,7 +288,7 @@ func (r *Repository) UpdateAttributes(ctx context.Context, origin app.ID, resour
} }
} }
resource, err = r.getResourceWithinTx(ctx, tx, origin, resourceID) resource, err = s.getResourceWithinTx(ctx, tx, origin, resourceID)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -303,7 +302,7 @@ func (r *Repository) UpdateAttributes(ctx context.Context, origin app.ID, resour
return resource, nil return resource, nil
} }
func (r *Repository) getResourceWithinTx(ctx context.Context, tx *sql.Tx, origin app.ID, resourceID share.ResourceID) (*share.BaseResource, error) { func (s *ShareStore) getResourceWithinTx(ctx context.Context, tx *sql.Tx, origin app.ID, resourceID share.ResourceID) (*share.BaseResource, error) {
query := ` query := `
SELECT name, type, value, created_at, updated_at SELECT name, type, value, created_at, updated_at
FROM resources FROM resources
@ -361,23 +360,23 @@ func (r *Repository) getResourceWithinTx(ctx context.Context, tx *sql.Tx, origin
return resource, nil return resource, nil
} }
func (r *Repository) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error { func (s *ShareStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
var db *sql.DB var db *sql.DB
db, err := r.getDB(ctx) db, err := s.getDB(ctx)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
if err := sqlite.WithTx(ctx, db, fn); err != nil { if err := WithTx(ctx, db, fn); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
return nil return nil
} }
func ensureTables(ctx context.Context, db *sql.DB) error { func ensureShareTables(ctx context.Context, db *sql.DB) error {
err := sqlite.WithTx(ctx, db, func(tx *sql.Tx) error { err := WithTx(ctx, db, func(tx *sql.Tx) error {
query := ` query := `
CREATE TABLE IF NOT EXISTS resources ( CREATE TABLE IF NOT EXISTS resources (
resource_id TEXT NOT NULL, resource_id TEXT NOT NULL,
@ -410,20 +409,20 @@ func ensureTables(ctx context.Context, db *sql.DB) error {
return nil return nil
} }
func NewRepository(path string) *Repository { func NewShareStore(path string) *ShareStore {
getDB := sqlite.NewGetDBFunc(path, ensureTables) getDB := NewGetDBFunc(path, ensureShareTables)
return &Repository{ return &ShareStore{
getDB: getDB, getDB: getDB,
} }
} }
func NewRepositoryWithDB(db *sql.DB) *Repository { func NewShareStoreWithDB(db *sql.DB) *ShareStore {
getDB := sqlite.NewGetDBFuncFromDB(db, ensureTables) getDB := NewGetDBFuncFromDB(db, ensureShareTables)
return &Repository{ return &ShareStore{
getDB: getDB, getDB: getDB,
} }
} }
var _ share.Repository = &Repository{} var _ share.Store = &ShareStore{}

View File

@ -7,18 +7,18 @@ import (
"testing" "testing"
"time" "time"
"forge.cadoles.com/arcad/edge/pkg/module/share" "forge.cadoles.com/arcad/edge/pkg/storage/share"
"forge.cadoles.com/arcad/edge/pkg/module/share/testsuite" "forge.cadoles.com/arcad/edge/pkg/storage/share/testsuite"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
func TestRepository(t *testing.T) { func TestRepository(t *testing.T) {
logger.SetLevel(logger.LevelDebug) logger.SetLevel(logger.LevelDebug)
testsuite.TestRepository(t, newTestRepo) testsuite.TestStore(t, newTestStore)
} }
func newTestRepo(testName string) (share.Repository, error) { func newTestStore(testName string) (share.Store, error) {
filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_")) filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
file := fmt.Sprintf("./testdata/%s.sqlite", filename) file := fmt.Sprintf("./testdata/%s.sqlite", filename)
@ -27,7 +27,7 @@ func newTestRepo(testName string) (share.Repository, error) {
} }
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds()) dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
repo := NewRepository(dsn) store := NewShareStore(dsn)
return repo, nil return store, nil
} }

View File

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

View File

@ -8,6 +8,16 @@ func (o *AndOperator) Token() Token {
return TokenAnd return TokenAnd
} }
func (o *AndOperator) AsMap() map[string]any {
children := make([]map[string]any, 0, len(o.children))
for _, c := range o.children {
children = append(children, c.AsMap())
}
return map[string]any{
string(TokenAnd): children,
}
}
func (o *AndOperator) Children() []Operator { func (o *AndOperator) Children() []Operator {
return o.children return o.children
} }

View File

@ -12,6 +12,16 @@ func (o *EqOperator) Fields() map[string]interface{} {
return o.fields return o.fields
} }
func (o *EqOperator) AsMap() map[string]any {
fields := make(map[string]any, len(o.fields))
for k, v := range o.fields {
fields[k] = v
}
return map[string]any{
string(TokenEq): fields,
}
}
func NewEqOperator(fields map[string]interface{}) *EqOperator { func NewEqOperator(fields map[string]interface{}) *EqOperator {
return &EqOperator{fields} return &EqOperator{fields}
} }

View File

@ -29,6 +29,15 @@ func NewFrom(raw map[string]interface{}) (*Filter, error) {
return &Filter{op}, nil return &Filter{op}, nil
} }
func (f *Filter) AsMap() map[string]any {
root := f.Root()
if root == nil {
return nil
}
return f.Root().AsMap()
}
func toFieldOperator(v interface{}) (Operator, error) { func toFieldOperator(v interface{}) (Operator, error) {
vv, ok := v.(map[string]interface{}) vv, ok := v.(map[string]interface{})
if !ok { if !ok {
@ -93,8 +102,17 @@ func toFieldOperator(v interface{}) (Operator, error) {
} }
func toAggregateOperator(token Token, v interface{}) (Operator, error) { func toAggregateOperator(token Token, v interface{}) (Operator, error) {
vv, ok := v.([]interface{}) var vv []interface{}
if !ok {
switch typed := v.(type) {
case []interface{}:
vv = typed
case []map[string]interface{}:
vv = make([]interface{}, 0, len(typed))
for _, item := range typed {
vv = append(vv, item)
}
default:
return nil, errors.WithStack(ErrInvalidAggregationOperator) return nil, errors.WithStack(ErrInvalidAggregationOperator)
} }

View File

@ -12,6 +12,16 @@ func (o *GtOperator) Fields() map[string]interface{} {
return o.fields return o.fields
} }
func (o *GtOperator) AsMap() map[string]any {
fields := make(map[string]any, len(o.fields))
for k, v := range o.fields {
fields[k] = v
}
return map[string]any{
string(TokenGt): fields,
}
}
func NewGtOperator(fields OperatorFields) *GtOperator { func NewGtOperator(fields OperatorFields) *GtOperator {
return &GtOperator{fields} return &GtOperator{fields}
} }

View File

@ -12,6 +12,16 @@ func (o *GteOperator) Fields() map[string]interface{} {
return o.fields return o.fields
} }
func (o *GteOperator) AsMap() map[string]any {
fields := make(map[string]any, len(o.fields))
for k, v := range o.fields {
fields[k] = v
}
return map[string]any{
string(TokenGte): fields,
}
}
func NewGteOperator(fields OperatorFields) *GteOperator { func NewGteOperator(fields OperatorFields) *GteOperator {
return &GteOperator{fields} return &GteOperator{fields}
} }

View File

@ -12,6 +12,16 @@ func (o *InOperator) Fields() map[string]interface{} {
return o.fields return o.fields
} }
func (o *InOperator) AsMap() map[string]any {
fields := make(map[string]any, len(o.fields))
for k, v := range o.fields {
fields[k] = v
}
return map[string]any{
string(TokenIn): fields,
}
}
func NewInOperator(fields OperatorFields) *InOperator { func NewInOperator(fields OperatorFields) *InOperator {
return &InOperator{fields} return &InOperator{fields}
} }

View File

@ -12,6 +12,16 @@ func (o *LikeOperator) Fields() map[string]interface{} {
return o.fields return o.fields
} }
func (o *LikeOperator) AsMap() map[string]any {
fields := make(map[string]any, len(o.fields))
for k, v := range o.fields {
fields[k] = v
}
return map[string]any{
string(TokenLike): fields,
}
}
func NewLikeOperator(fields OperatorFields) *LikeOperator { func NewLikeOperator(fields OperatorFields) *LikeOperator {
return &LikeOperator{fields} return &LikeOperator{fields}
} }

View File

@ -12,6 +12,16 @@ func (o *LtOperator) Fields() map[string]interface{} {
return o.fields return o.fields
} }
func (o *LtOperator) AsMap() map[string]any {
fields := make(map[string]any, len(o.fields))
for k, v := range o.fields {
fields[k] = v
}
return map[string]any{
string(TokenLt): fields,
}
}
func NewLtOperator(fields OperatorFields) *LtOperator { func NewLtOperator(fields OperatorFields) *LtOperator {
return &LtOperator{fields} return &LtOperator{fields}
} }

View File

@ -12,6 +12,16 @@ func (o *LteOperator) Fields() map[string]interface{} {
return o.fields return o.fields
} }
func (o *LteOperator) AsMap() map[string]any {
fields := make(map[string]any, len(o.fields))
for k, v := range o.fields {
fields[k] = v
}
return map[string]any{
string(TokenLte): fields,
}
}
func NewLteOperator(fields OperatorFields) *LteOperator { func NewLteOperator(fields OperatorFields) *LteOperator {
return &LteOperator{fields} return &LteOperator{fields}
} }

View File

@ -12,6 +12,16 @@ func (o *NeqOperator) Fields() map[string]interface{} {
return o.fields return o.fields
} }
func (o *NeqOperator) AsMap() map[string]any {
fields := make(map[string]any, len(o.fields))
for k, v := range o.fields {
fields[k] = v
}
return map[string]any{
string(TokenNeq): fields,
}
}
func NewNeqOperator(fields map[string]interface{}) *NeqOperator { func NewNeqOperator(fields map[string]interface{}) *NeqOperator {
return &NeqOperator{fields} return &NeqOperator{fields}
} }

View File

@ -12,6 +12,16 @@ func (o *NotOperator) Children() []Operator {
return o.children return o.children
} }
func (o *NotOperator) AsMap() map[string]any {
children := make([]map[string]any, 0, len(o.children))
for _, c := range o.children {
children = append(children, c.AsMap())
}
return map[string]any{
string(TokenNot): children,
}
}
func NewNotOperator(ops ...Operator) *NotOperator { func NewNotOperator(ops ...Operator) *NotOperator {
return &NotOperator{ops} return &NotOperator{ops}
} }

View File

@ -20,4 +20,15 @@ type OperatorFields map[string]interface{}
type Operator interface { type Operator interface {
Token() Token Token() Token
AsMap() map[string]any
}
type FieldOperator interface {
Operator
Fields() map[string]any
}
type AggregatorOperator interface {
Operator
Children() []Operator
} }

View File

@ -12,6 +12,16 @@ func (o *OrOperator) Children() []Operator {
return o.children return o.children
} }
func (o *OrOperator) AsMap() map[string]any {
children := make([]map[string]any, 0, len(o.children))
for _, c := range o.children {
children = append(children, c.AsMap())
}
return map[string]any{
string(TokenOr): children,
}
}
func NewOrOperator(ops ...Operator) *OrOperator { func NewOrOperator(ops ...Operator) *OrOperator {
return &OrOperator{ops} return &OrOperator{ops}
} }

View File

@ -7,7 +7,7 @@ type FindResourcesOptions struct {
ValueType *ValueType ValueType *ValueType
} }
func FillFindResourcesOptions(funcs ...FindResourcesOptionFunc) *FindResourcesOptions { func NewFindResourcesOptions(funcs ...FindResourcesOptionFunc) *FindResourcesOptions {
opts := &FindResourcesOptions{} opts := &FindResourcesOptions{}
for _, fn := range funcs { for _, fn := range funcs {

View File

@ -23,7 +23,7 @@ type Attribute interface {
CreatedAt() time.Time CreatedAt() time.Time
} }
type Repository interface { type Store interface {
DeleteResource(ctx context.Context, origin app.ID, resourceID ResourceID) error DeleteResource(ctx context.Context, origin app.ID, resourceID ResourceID) error
FindResources(ctx context.Context, funcs ...FindResourcesOptionFunc) ([]Resource, error) FindResources(ctx context.Context, funcs ...FindResourcesOptionFunc) ([]Resource, error)
GetResource(ctx context.Context, origin app.ID, resourceID ResourceID) (Resource, error) GetResource(ctx context.Context, origin app.ID, resourceID ResourceID) (Resource, error)

Some files were not shown because too many files have changed in this diff Show More