Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
2023-08-16 13:17:42 +03:00
parent 0678e72908
commit 78f0ae14d7
70 changed files with 4159 additions and 2355 deletions

64
internal/config/config.go Normal file
View File

@@ -0,0 +1,64 @@
package config
import "time"
type AppConfig struct{}
type ServerConfig struct {
Name string `json:"name" yaml:"name"`
Version string `json:"-" yaml:"-"`
Addr string `json:"addr" yaml:"addr" default:":9090"`
Crt string `json:"crt" yaml:"crt"`
Key string `json:"key" yaml:"key"`
ID string `json:"-" yaml:"-" default:"micro:generate uuid"`
LoggerLevel string `json:"logger_level" yaml:"logger_level"`
}
type TracerConfig struct {
Metadata map[string]string `json:"metadata" yaml:"metadata"`
AgentHost string `env:"JAEGER_AGENT_HOST" json:"host" yaml:"host" default:"127.0.0.1"`
AgentPort string `env:"JAEGER_AGENT_PORT" json:"port" yaml:"port" default:"6831"`
Collector string `env:"JAEGER_ENDPOINT,TRACER_ENDPOINT" json:"endpoint" yaml:"endpoint"`
}
type VaultConfig struct {
Addr string `env:"VAULT_ADDR" json:"addr" yaml:"addr" default:"http://127.0.0.1:8200"`
Token string `env:"VAULT_TOKEN" json:"-" yaml:"-"`
Path string `env:"VAULT_PATH" json:"-" yaml:"-" default:"apigw/data/authn"`
}
type MeterConfig struct {
Addr string `json:"addr" yaml:"addr" default:"0.0.0.0:8080"`
Path string `json:"path" yaml:"path" default:"/metrics"`
}
type DatabaseConfig struct {
DSN string `json:"dsn" yaml:"dsn"`
Type string `json:"-" yaml:"-"`
Migrate string `json:"-" yaml:"-"`
ConnStr string `json:"-" yaml:"-"`
MaxOpenConns int `json:"-" yaml:"-"`
MaxIdleConns int `json:"-" yaml:"-"`
ConnMaxLifetime time.Duration `json:"-" yaml:"-"`
ConnMaxIdleTime time.Duration `json:"-" yaml:"-"`
MigrateForce bool `json:"-" yaml:"-"`
}
type Config struct {
App *AppConfig `json:"app" yaml:"app"`
Database *DatabaseConfig `json:"database" yaml:"database"`
Server *ServerConfig `json:"server" yaml:"server"`
Meter *MeterConfig `json:"meter" yaml:"meter"`
Vault *VaultConfig `json:"-" yaml:"-"`
Tracer *TracerConfig `json:"tracer" yaml:"tracer"`
}
func NewConfig(name, version string) *Config {
return &Config{
App: &AppConfig{},
Server: &ServerConfig{Name: name, Version: version},
Tracer: &TracerConfig{},
Meter: &MeterConfig{},
Vault: &VaultConfig{},
}
}

View File

@@ -0,0 +1,40 @@
package handler
import (
"context"
"database/sql"
"errors"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v4"
"git.unistack.org/unistack-org/pkgdash/internal/models"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func (h *Handler) CommentsCreate(ctx context.Context, req *pb.CommentsCreateReq, rsp *pb.CommentsCreateRsp) error {
logger := h.svc.Logger()
logger.Debug(ctx, "Start AddComment")
err := req.Validate()
if err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
var com *models.Comment
if com, err = h.store.CommentsCreate(ctx, req); err != nil {
logger.Error(ctx, err)
if errors.Is(err, sql.ErrNoRows) {
httpsrv.SetRspCode(ctx, http.StatusNotFound)
return httpsrv.SetError(NewNotFoundError(err))
}
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
rsp.Comment = models.NewComment(com)
logger.Debug(ctx, "Success finish addComment")
return nil
}

View File

@@ -0,0 +1,36 @@
package handler
import (
"context"
"database/sql"
"errors"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v4"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func (h *Handler) CommentsDelete(ctx context.Context, req *pb.CommentsDeleteReq, rsp *pb.CommentsDeleteRsp) error {
logger := h.svc.Logger()
logger.Debug(ctx, "Start AddComment")
err := req.Validate()
if err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
if err = h.store.CommentsDelete(ctx, req); err != nil {
logger.Error(ctx, err)
if errors.Is(err, sql.ErrNoRows) {
httpsrv.SetRspCode(ctx, http.StatusNotFound)
return httpsrv.SetError(NewNotFoundError(err))
}
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
logger.Debug(ctx, "Success finish addComment")
return nil
}

View File

@@ -0,0 +1,36 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v4"
"git.unistack.org/unistack-org/pkgdash/internal/models"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func (h *Handler) CommentsList(ctx context.Context, req *pb.CommentsListReq, rsp *pb.CommentsListRsp) error {
logger := h.svc.Logger()
logger.Debug(ctx, "Start GetModule")
err := req.Validate()
if err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
comments, err := h.store.CommentsList(ctx, req)
if err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
for _, com := range comments {
rsp.Comments = append(rsp.Comments, models.NewComment(com))
}
logger.Debug(ctx, "Success finish getModule")
return nil
}

View File

@@ -0,0 +1,11 @@
package handler
import (
"context"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func (h *Handler) CommentsLookup(ctx context.Context, req *pb.CommentsLookupReq, rsp *pb.CommentsLookupRsp) error {
return nil
}

View File

@@ -0,0 +1,76 @@
package handler
import (
"context"
"errors"
"net/http"
"strconv"
"github.com/google/uuid"
"go.unistack.org/micro/v4"
cligit "git.unistack.org/unistack-org/pkgdash/internal/service/client_git"
"git.unistack.org/unistack-org/pkgdash/internal/storage"
pb "git.unistack.org/unistack-org/pkgdash/proto"
"google.golang.org/protobuf/encoding/protojson"
)
type Handler struct {
svc micro.Service
store storage.Storage
protojson.MarshalOptions
protojson.UnmarshalOptions
git cligit.Client
chanUrl chan *pb.PackagesCreateReq
}
func NewNotFoundError(err error) *pb.ErrorRsp {
return &pb.ErrorRsp{
Code: strconv.Itoa(http.StatusBadRequest),
Title: "NotFound",
Uuid: uuid.New().String(),
Details: err.Error(),
}
}
func NewInternalError(err error) *pb.ErrorRsp {
return &pb.ErrorRsp{
Code: strconv.Itoa(http.StatusInternalServerError),
Title: "InternalServerError",
Uuid: uuid.New().String(),
Details: err.Error(),
}
}
func NewValidationError(err error) *pb.ErrorRsp {
return &pb.ErrorRsp{
Code: strconv.Itoa(http.StatusBadRequest),
Title: "BadRequest",
Uuid: uuid.New().String(),
Details: err.Error(),
}
}
func NewHandler(svc micro.Service, client cligit.Client) *Handler {
h := &Handler{
svc: svc,
git: client,
}
h.EmitUnpopulated = true
h.UseProtoNames = false
return h
}
func (h *Handler) Init(ctx context.Context) error {
store, err := storage.FromContext(h.svc.Options().Context)
if err != nil {
return errors.New("missing storage")
}
h.chanUrl = h.git.Run(ctx, store)
h.store = store
return nil
}

View File

@@ -0,0 +1,35 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v4"
"git.unistack.org/unistack-org/pkgdash/internal/models"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func (h *Handler) ModulesList(ctx context.Context, req *pb.ModulesListReq, rsp *pb.ModulesListRsp) error {
logger := h.svc.Logger()
logger.Debug(ctx, "Start GetModule")
err := req.Validate()
if err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
modules, err := h.store.ModulesList(ctx, req)
if err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
for _, mod := range modules {
rsp.Modules = append(rsp.Modules, models.NewModule(mod))
}
logger.Debug(ctx, "Success finish getModule")
return nil
}

View File

@@ -0,0 +1,32 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v4"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func (h *Handler) PackagesCreate(ctx context.Context, req *pb.PackagesCreateReq, rsp *pb.PackagesCreateRsp) error {
logger := h.svc.Logger()
logger.Debug(ctx, "Start AddPackage")
err := req.Validate()
if err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
if h.git.IsClose() {
logger.Error(ctx, "chan is closed")
} else {
h.chanUrl <- req
}
rsp.Status = "Sent"
logger.Debug(ctx, "Success finish addPackage")
return nil
}

View File

@@ -0,0 +1,29 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v4"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func (h *Handler) PackagesDelete(ctx context.Context, req *pb.PackagesDeleteReq, rsp *pb.PackagesDeleteRsp) error {
logger := h.svc.Logger()
logger.Debug(ctx, "Start UpdatePackage")
if err := req.Validate(); err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
if err := h.store.PackagesDelete(ctx, req); err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
logger.Debug(ctx, "Success finish UpdatePackage")
return nil
}

View File

@@ -0,0 +1,28 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v4"
"git.unistack.org/unistack-org/pkgdash/internal/models"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func (h *Handler) PackagesList(ctx context.Context, req *pb.PackagesListReq, rsp *pb.PackagesListRsp) error {
logger := h.svc.Logger()
logger.Debug(ctx, "Start getListPackage")
packages, err := h.store.PackagesList(ctx, req)
if err != nil {
logger.Errorf(ctx, "error db response: %v", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
for _, pkg := range packages {
rsp.Packages = append(rsp.Packages, models.NewPackage(pkg))
}
logger.Debug(ctx, "Success finish getListPackage")
return nil
}

View File

@@ -0,0 +1,31 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v4"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func (h *Handler) PackagesUpdate(ctx context.Context, req *pb.PackagesUpdateReq, rsp *pb.PackagesUpdateRsp) error {
logger := h.svc.Logger()
logger.Debug(ctx, "Start UpdatePackage")
if err := req.Validate(); err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
if err := h.store.PackagesUpdate(ctx, req); err != nil {
logger.Error(ctx, err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
// rsp.Id = req.Id
logger.Debug(ctx, "Success finish UpdatePackage")
return nil
}

78
internal/models/models.go Normal file
View File

@@ -0,0 +1,78 @@
package models
import (
"time"
pb "git.unistack.org/unistack-org/pkgdash/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
type Package struct {
Name string `db:"name" json:"name"`
URL string `db:"url" json:"url"`
Modules []uint64 `db:"modules" json:"modules"`
Issues []uint64 `db:"issues" json:"issues,omitempty"`
Comments []uint64 `db:"comments" json:"comments,omitempty"`
ID uint64 `db:"id" json:"id"`
Created time.Time `db:"created" json:"created"`
Updated time.Time `db:"updated" json:"updated,omitempty"`
}
func NewPackage(pkg *Package) *pb.Package {
return &pb.Package{
Name: pkg.Name,
Url: pkg.URL,
Modules: pkg.Modules,
Issues: pkg.Issues,
Comments: pkg.Comments,
Id: pkg.ID,
Created: timestamppb.New(pkg.Created),
Updated: timestamppb.New(pkg.Updated),
}
}
type Module struct {
Name string `db:"name"`
Version string `db:"version"`
LastVersion string `db:"last_version"`
ID uint64 `db:"id"`
Package uint64 `db:"package"`
Created time.Time `db:"created" json:"created"`
Updated time.Time `db:"updated" json:"updated,omitempty"`
}
func NewModule(mod *Module) *pb.Module {
return &pb.Module{
Name: mod.Name,
Version: mod.Version,
LastVersion: mod.LastVersion,
Package: mod.Package,
Id: mod.ID,
Created: timestamppb.New(mod.Created),
Updated: timestamppb.New(mod.Updated),
}
}
type Issue struct {
Desc string `db:"desc"`
Modules []int64 `db:"modules"`
ID uint64 `db:"id"`
Status uint64 `db:"status"`
Package int64 `db:"package"`
}
type Comment struct {
Created time.Time `db:"created" json:"created"`
Updated time.Time `db:"updated" json:"updated,omitempty"`
Text string `db:"value" json:"text"`
ID uint64 `db:"id" json:"id"`
}
func NewComment(com *Comment) *pb.Comment {
return &pb.Comment{
Id: com.ID,
Text: com.Text,
Created: timestamppb.New(com.Created),
Updated: timestamppb.New(com.Updated),
}
}

View File

@@ -0,0 +1,218 @@
package client_git
import (
"context"
"fmt"
"io"
"net/url"
"os"
"sort"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
"go.unistack.org/micro/v4/logger"
"git.unistack.org/unistack-org/pkgdash/internal"
"git.unistack.org/unistack-org/pkgdash/internal/models"
"git.unistack.org/unistack-org/pkgdash/internal/storage"
pb "git.unistack.org/unistack-org/pkgdash/proto"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
)
type Client interface {
Run(ctx context.Context, st storage.Storage) chan *pb.PackagesCreateReq
IsClose() bool
Done() <-chan struct{}
}
type client struct {
worker chan *pb.PackagesCreateReq
closed bool
lock chan struct{}
}
func NewClient(cap uint) Client {
return &client{
make(chan *pb.PackagesCreateReq, cap),
false,
make(chan struct{}),
}
}
func (c *client) Run(ctx context.Context, st storage.Storage) chan *pb.PackagesCreateReq {
go func() {
defer close(c.worker)
for {
select {
case data := <-c.worker:
go func() {
runner(ctx, st, data)
}()
case <-ctx.Done():
logger.Info(ctx, ctx.Err())
return
}
}
}()
return c.worker
}
func (c *client) IsClose() bool {
return c.closed
}
// Done for locked goroutine
func (c *client) Done() <-chan struct{} {
return c.lock
}
func runner(ctx context.Context, st storage.Storage, req *pb.PackagesCreateReq) {
modules, err := getGoModule(ctx, req.Url)
if err != nil {
logger.Error(ctx, err)
return
}
logger.Infof(ctx, "success get list mod", modules)
if req.Modules, err = st.InsertButchModules(ctx, modules); err != nil {
logger.Error(ctx, err)
return
}
if err = st.PackagesCreate(ctx, req); err != nil {
logger.Error(ctx, err)
}
}
func getGoModule(ctx context.Context, gitUrl string) ([]models.Module, error) {
u, err := url.Parse(gitUrl)
if err != nil {
logger.Fatal(ctx, err)
}
var rev string
if idx := strings.Index(u.Path, "@"); idx > 0 {
rev = u.Path[idx+1:]
}
cloneOpts := &git.CloneOptions{
URL: gitUrl,
Progress: os.Stdout,
}
if len(rev) == 0 {
cloneOpts.SingleBranch = true
cloneOpts.Depth = 1
}
if err = cloneOpts.Validate(); err != nil {
return nil, err
}
repo, err := git.CloneContext(ctx, memory.NewStorage(), nil, cloneOpts)
if err != nil {
return nil, err
}
ref, err := repo.Head()
if err != nil {
return nil, fmt.Errorf("failed to get head: %v", err)
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
return nil, fmt.Errorf("failed to get commit: %v", err)
}
tree, err := commit.Tree()
if err != nil {
return nil, err
}
unique := make(map[string]models.Module)
var mvs []module.Version
err = tree.Files().ForEach(func(file *object.File) error {
if file == nil {
err = errors.New("file pointer is nil")
logger.Error(ctx, err)
return err
}
switch file.Mode {
case filemode.Regular:
if strings.HasSuffix(file.Name, "go.mod") {
if mvs, err = Direct(file); err != nil {
return err
}
for i := range mvs {
unique[mvs[i].Path] = models.Module{
Name: mvs[i].Path,
Version: mvs[i].Version,
LastVersion: mvs[i].Version,
}
}
internal.Updates(internal.UpdateOptions{
Pre: false,
Major: false,
Cached: false,
Modules: mvs,
OnUpdate: func(u internal.Update) {
if u.Err != nil {
logger.Errorf(ctx, "%s: failed: %v\n", u.Module.Path, u.Err)
} else {
val := unique[u.Module.Path]
val.LastVersion = u.Version
unique[u.Module.Path] = val
}
},
})
}
}
return nil
})
result := make([]models.Module, 0, len(unique))
for _, v := range unique {
result = append(result, v)
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result, err
}
func Direct(file *object.File) ([]module.Version, error) {
r, err := file.Reader()
if err != nil {
return nil, err
}
defer r.Close()
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
modfile, err := modfile.ParseLax("go.mod", data, nil)
if err != nil {
return nil, err
}
var mods []module.Version
for _, req := range modfile.Require {
// if !req.Indirect {
mods = append(mods, req.Mod)
//}
}
/*
sort.Slice(mods, func(i, j int) bool {
return mods[i].Path < mods[j].Path
})
*/
return mods, nil
}

View File

@@ -0,0 +1,81 @@
package client_git
import (
"context"
"database/sql"
"embed"
"fmt"
"testing"
"git.unistack.org/unistack-org/pkgdash/internal/storage"
// "git.unistack.org/unistack-org/pkgdash/internal/storage/postgres"
"git.unistack.org/unistack-org/pkgdash/internal/storage/sqlite"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func TestClientPG(t *testing.T) {
dsn := fmt.Sprintf("file:///database.db")
conn, err := sql.Open("sqlite", dsn)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
if err = conn.Ping(); err != nil {
t.Fatal(err)
}
fucntion := sqlite.NewStorage()
st := fucntion(conn, embed.FS{})
s, ok := st.(storage.Storage)
if !ok {
t.Fatal("typecast error")
}
ctx, cancel := context.WithCancel(context.Background())
_ = cancel
cli := NewClient(1)
ch := cli.Run(ctx, s)
data := &pb.AddPackageReq{
Name: "test",
Url: "https://github.com/dantedenis/service_history.git",
}
ch <- data
<-cli.Done()
}
func TestClientLite(t *testing.T) {
conn, err := sql.Open("sqlite3", "../../identifier.sqlite")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
if err = conn.Ping(); err != nil {
t.Fatal(err)
}
function := sqlite.NewStorage()
st := function(conn, embed.FS{})
s, ok := st.(storage.Storage)
if !ok {
t.Fatal("typecast error")
}
ctx, cancel := context.WithCancel(context.Background())
_ = cancel
cli := NewClient(1)
ch := cli.Run(ctx, s)
data := &pb.AddPackageReq{
Name: "test",
Url: "https://github.com/dantedenis/service_history.git",
}
ch <- data
<-cli.Done()
}

154
internal/service/service.go Normal file
View File

@@ -0,0 +1,154 @@
package service
import (
"context"
"database/sql"
"net/url"
"strings"
httpsrv "go.unistack.org/micro-server-http/v4" // TODO
"go.unistack.org/micro/v4"
"go.unistack.org/micro/v4/config"
microcfg "go.unistack.org/micro/v4/config"
"go.unistack.org/micro/v4/logger"
"go.unistack.org/micro/v4/options"
"go.unistack.org/micro/v4/register"
"go.unistack.org/micro/v4/server"
intcfg "git.unistack.org/unistack-org/pkgdash/config"
"git.unistack.org/unistack-org/pkgdash/handler"
pb "git.unistack.org/unistack-org/pkgdash/proto"
"git.unistack.org/unistack-org/pkgdash/service/client_git"
"git.unistack.org/unistack-org/pkgdash/storage"
)
func NewService(ctx context.Context) (micro.Service, error) {
var reg register.Register
cfg := intcfg.NewConfig(ServiceName, Service)
cs := microcfg.NewConfig(config.Struct(cfg))
// TODO
mgsrv := httpsrv.NewServer(
options.Register(reg),
)
svc := micro.NewService(
micro.Config(cs),
)
h := handler.NewHandler(svc, client_git.NewClient(5))
if err := svc.Init(
micro.AfterStart(func(_ context.Context) error {
return h.Init(svc.Options().Context)
}),
micro.BeforeStart(func(ctx context.Context) error {
if err := config.Load(ctx, []config.Config{cs}, config.LoadOverride(true)); err != nil {
return err
}
if err := config.Validate(ctx, cfg); err != nil {
return err
}
if err := svc.Init(
micro.Name(intcfg.ServiceName),
micro.Version(intcfg.ServiceVersion),
); err != nil {
return err
}
if err := svc.Server("http").Init(
options.Address(cfg.Address),
options.Name(cfg.App.Name),
server.Version(cfg.App.Version),
); err != nil {
return err
}
return nil
}),
micro.BeforeStart(func(_ context.Context) error {
log := logger.NewLogger(
logger.WithLevel(logger.ParseLevel(cfg.LogLevel)),
logger.WithCallerSkipCount(3),
)
return svc.Init(micro.Logger(log))
}),
micro.BeforeStart(func(ctx context.Context) error {
var connstr string
if v, ok := cfg.StorageDSN[cfg.App.Name]; ok {
connstr = v
} else if v, ok = cfg.StorageDSN["all"]; ok {
connstr = v
}
scheme, dsn, err := storageOptions(connstr)
if err != nil {
return err
}
conn, err := connectDataBase(scheme, dsn)
if err != nil {
return err
}
store, err := storage.NewStorage(scheme, conn)
if err != nil {
return err
}
ctx = storage.InContext(ctx, store)
return svc.Init(micro.Context(ctx))
}),
); err != nil {
return nil, err
}
if err := pb.RegisterPkgdashServiceServer(mgsrv, h); err != nil {
logger.Fatalf(ctx, "failed to register handler: %v", err)
}
intsvc := httpsrv.NewServer(
server.Codec("application/json", jsoncodec.NewCodec()),
server.Address(cfg.Meter.Addr), server.Context(ctx),
)
if err := intsvc.Init(); err != nil {
logger.Fatalf(ctx, "failed to init http srv: %v", err)
}
if err := healthhandler.RegisterHealthServiceServer(intsvc, healthhandler.NewHandler()); err != nil {
logger.Fatalf(ctx, "failed to set http handler: %v", err)
}
if err := meterhandler.RegisterMeterServiceServer(intsvc, meterhandler.NewHandler()); err != nil {
logger.Fatalf(ctx, "failed to set http handler: %v", err)
}
if err := intsvc.Start(); err != nil {
logger.Fatalf(ctx, "failed to run http srv: %v", err)
}
return svc, nil
}
func storageOptions(dsn string) (string, string, error) {
u, err := url.Parse(dsn)
if err != nil {
return "", "", err
}
scheme := u.Scheme
if idx := strings.Index(u.Scheme, "+"); idx > 0 {
scheme = u.Scheme[:idx]
u.Scheme = u.Scheme[idx+1:]
}
return scheme, u.String(), nil
}
func connectDataBase(driverName, dsn string) (*sql.DB, error) {
conn, err := sql.Open(driverName, dsn)
if err != nil {
return nil, err
}
if err = conn.Ping(); err != nil {
return nil, err
}
return conn, err
}

View File

@@ -0,0 +1 @@
drop table if exists dashboard, package, module, issue, comment;

View File

@@ -0,0 +1,40 @@
create table if not exists dashboard (
id serial not null unique primary key ,
"uuid" uuid not null unique default gen_random_uuid() ,
package integer[] default '{}'::integer[]
);
create table if not exists comment (
id serial not null unique primary key ,
"text" text ,
package integer not null,
created timestamp not null default current_timestamp ,
updated timestamp default current_timestamp
);
create table if not exists module (
id serial not null unique primary key ,
name varchar not null ,
version varchar not null ,
last_version varchar not null
);
create table if not exists issue (
id serial not null unique primary key ,
--package integer references package(id) ,
modules integer[] default '{}'::integer[],
status integer default 0 ,
"desc" varchar
);
create table if not exists package (
id serial not null unique primary key ,
name varchar not null ,
url varchar ,
modules integer[] default '{}'::integer[],
issues integer[] default '{}'::integer[],
comments integer[] default '{}'::integer[]
);
create unique index module_info on module(name, version);

View File

@@ -0,0 +1,5 @@
drop table if exists dashboard ;
drop table if exists package ;
drop table if exists module ;
drop table if exists issue ;
drop table if exists comment;

View File

@@ -0,0 +1,39 @@
create table if not exists dashboard (
id integer primary key autoincrement not null ,
"uuid" uuid not null unique ,
package integer[] default '{}'
);
create table if not exists comment (
id integer primary key autoincrement not null ,
"text" text ,
package integer not null,
created timestamp not null default current_timestamp ,
updated timestamp default current_timestamp
);
create table if not exists module (
id integer primary key autoincrement not null ,
name varchar not null ,
version varchar not null ,
last_version varchar not null
);
create table if not exists issue (
id serial not null unique primary key ,
--package integer references package(id) ,
modules integer[] default '[]',
status integer default 0 ,
"desc" varchar
);
create table if not exists package (
id integer primary key autoincrement not null ,
name varchar not null ,
url varchar ,
modules integer[] default '[]',
issues integer[] default '[]',
comments integer[] default '[]'
);

View File

@@ -0,0 +1,32 @@
package postgres
const (
queryListPackage = `
select
id,
name,
url,
comments
--modules,
--issues,
from package;
`
queryAddComment = `
with insert_comm as (
insert into comment(text) values ($1) returning id
)
update package set comments = array_append(comments, (select * from insert_comm)) where id=$2;
`
queryAddPackage = `
insert into package(name, url, modules) values ($1, $2, $3);
`
queryInsMsgGetIDs = `
insert into module(name, version, last_version) values
%s
returning id;
`
queryGetModule = `
select id, name, version, last_version from module
where id in %s ;
`
)

View File

@@ -0,0 +1,233 @@
package postgres
import (
"context"
"database/sql"
"embed"
"errors"
"fmt"
"strings"
"github.com/golang-migrate/migrate/v4"
mpgx "github.com/golang-migrate/migrate/v4/database/pgx"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/lib/pq"
"go.unistack.org/micro/v4/logger"
"git.unistack.org/unistack-org/pkgdash/internal/config"
"git.unistack.org/unistack-org/pkgdash/internal/models"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
const (
pathMigration = `migrations/postgres`
)
type Postgres struct {
db *sql.DB
fs embed.FS
}
func NewStorage() func(*sql.DB, embed.FS) interface{} {
return func(db *sql.DB, fs embed.FS) interface{} {
return &Postgres{db: db, fs: fs}
}
}
func (s *Postgres) MigrateUp() error {
driver, err := mpgx.WithInstance(s.db, &mpgx.Config{
MigrationsTable: mpgx.DefaultMigrationsTable,
DatabaseName: config.ServiceName,
})
if err != nil {
return err
}
source, err := iofs.New(s.fs, pathMigration)
if err != nil {
return err
}
// TODO: pass own logger
m, err := migrate.NewWithInstance("fs", source, config.ServiceName, driver)
if err != nil {
return err
}
if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
func (s *Postgres) MigrateDown() error {
driver, err := mpgx.WithInstance(s.db, &mpgx.Config{
MigrationsTable: mpgx.DefaultMigrationsTable,
DatabaseName: config.ServiceName,
})
if err != nil {
return err
}
source, err := iofs.New(s.fs, pathMigration)
if err != nil {
return err
}
// TODO: pass own logger
m, err := migrate.NewWithInstance("fs", source, config.ServiceName, driver)
if err != nil {
return err
}
if err = m.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
func (s *Postgres) PackagesUpdate(ctx context.Context, req *pb.PackagesUpdateReq) error {
panic("need implement")
}
func (s *Postgres) PackagesList(ctx context.Context, req *pb.PackagesListReq) (models.ListPackage, error) {
rows, err := s.db.QueryContext(ctx, queryListPackage)
if err != nil {
return nil, err
}
defer func() {
if err = rows.Close(); err != nil {
return
}
err = rows.Err()
}()
result := make([]*models.Package, 0)
for rows.Next() {
tmp := &models.Package{}
if err = rows.Scan(
&tmp.ID,
&tmp.Name,
&tmp.URL,
pq.Array(&tmp.Comments),
); err != nil {
return nil, err
}
}
return result, err
}
func (s *Postgres) CommentsCreate(ctx context.Context, req *pb.CommentsCreateReq) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
logger.Errorf(ctx, "AddComment: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
res, err := tx.ExecContext(ctx, queryAddComment, req.Text, req.PackageId)
if err != nil {
return err
}
if aff, affErr := res.RowsAffected(); err != nil {
err = affErr
} else if aff == 0 {
err = errors.New("rows affected is 0")
}
return err
}
func (s *Postgres) PackagesCreate(ctx context.Context, req *pb.PackagesCreateReq) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
logger.Errorf(ctx, "AddPackage: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
res, err := tx.ExecContext(ctx, queryAddPackage, req.Name, req.Url, pq.Array(req.Modules))
if err != nil {
return err
}
if aff, affErr := res.RowsAffected(); err != nil {
err = affErr
} else if aff == 0 {
err = errors.New("rows affected is 0")
}
return err
}
func (s *Postgres) InsertButchModules(ctx context.Context, req []models.Module) ([]uint64, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
logger.Errorf(ctx, "AddPackage: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
query := generateQuery(req)
rows, err := tx.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer func() {
if err = rows.Close(); err != nil {
return
}
err = rows.Err()
}()
result := make([]uint64, 0)
for rows.Next() {
tmp := uint64(0)
if err = rows.Scan(&tmp); err != nil {
return nil, err
}
result = append(result, tmp)
}
return result, err
}
func generateQuery(rsp []models.Module) string {
const pattern = `%c('%s', '%s', '%s')`
build := strings.Builder{}
comma := ' '
for i := range rsp {
str := fmt.Sprintf(pattern, comma, rsp[i].Name, rsp[i].Version, rsp[i].LastVersion)
build.WriteString(str)
comma = ','
}
return fmt.Sprintf(queryInsMsgGetIDs, build.String())
}

View File

@@ -0,0 +1,38 @@
package postgres
import (
"fmt"
"testing"
"git.unistack.org/unistack-org/pkgdash/internal/models"
)
func TestGenerate(t *testing.T) {
m := []models.Module{
{
ID: 1,
Name: "test",
Version: "1.2.3",
Package: 2,
LastVersion: "23.31",
},
{
ID: 1,
Name: "321test",
Version: "1.3",
Package: 4,
LastVersion: "2111.31",
},
{
ID: 1,
Name: "testabcd",
Version: "1.2.3",
Package: 2,
LastVersion: "153453.31",
},
}
str := generateQuery(m)
fmt.Println(str)
}

View File

@@ -0,0 +1,34 @@
package sqlite
const (
queryPackagesList = `select
id,
name,
url,
comments
modules,
issues,
from package;
`
queryCommentsCreate = `
insert into comment(text) values ($1) returning id;
update package
set comments = json_insert(comments, '$[#]', ( select last_insert_rowid() as id from comment ))
where id = $2 ;
`
queryPackagesCreate = `
insert into package(name, url, modules) values ($1, $2, $3);
`
queryInsMsgGetIDs = `
insert into module(name, version, last_version) values
%s
returning id;
`
queryModulesList = `
select id, name, version, last_version, created, updated from modules;
`
queryCommentsList = `
select id, text, created, updated from comments;
`
)

View File

@@ -0,0 +1,321 @@
package sqlite
import (
"context"
"database/sql"
"embed"
"errors"
"fmt"
"strings"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"go.unistack.org/micro/v4/logger"
"git.unistack.org/unistack-org/pkgdash/internal/models"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
const (
pathMigration = `migrations/sqlite`
)
type Sqlite struct {
db *sql.DB
fs embed.FS
}
func NewStorage() func(*sql.DB, embed.FS) interface{} {
return func(db *sql.DB, fs embed.FS) interface{} {
return &Sqlite{db: db, fs: fs}
}
}
func (s *Sqlite) MigrateUp() error {
driver, err := sqlite.WithInstance(s.db, &sqlite.Config{
MigrationsTable: sqlite.DefaultMigrationsTable,
DatabaseName: "pkgdash",
})
if err != nil {
return err
}
source, err := iofs.New(s.fs, pathMigration)
if err != nil {
return err
}
// TODO: pass own logger
m, err := migrate.NewWithInstance("fs", source, "pkgdash", driver)
if err != nil {
return err
}
if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
func (s *Sqlite) MigrateDown() error {
driver, err := sqlite.WithInstance(s.db, &sqlite.Config{
MigrationsTable: sqlite.DefaultMigrationsTable,
DatabaseName: "pkgdash",
})
if err != nil {
return err
}
source, err := iofs.New(s.fs, pathMigration)
if err != nil {
return err
}
// TODO: pass own logger
m, err := migrate.NewWithInstance("fs", source, "pkgdash", driver)
if err != nil {
return err
}
if err = m.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
func (s *Sqlite) PackagesUpdate(ctx context.Context, req *pb.PackagesUpdateReq) error {
panic("need implement")
}
func (s *Sqlite) PackagesList(ctx context.Context, req *pb.PackagesListReq) ([]*models.Package, error) {
var packages []*models.Package
rows, err := s.db.QueryContext(ctx, queryPackagesList)
if err != nil {
return nil, err
}
for ; rows.Err() == nil; rows.Next() {
pkg := &models.Package{}
if err = rows.Scan(
&pkg.ID,
&pkg.Name,
&pkg.URL,
&pkg.Comments,
); err != nil {
_ = rows.Close()
return nil, err
}
packages = append(packages, pkg)
}
if err = rows.Err(); err != nil {
return nil, err
}
if err = rows.Close(); err != nil {
return nil, err
}
return packages, err
}
func (s *Sqlite) CommentsCreate(ctx context.Context, req *pb.CommentsCreateReq) (id uint64, err error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return 0, err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
logger.Errorf(ctx, "AddComment: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
if err = tx.QueryRowContext(ctx, queryCommentsCreate, req.Text, req.PackageId).Scan(&id); err != nil {
return id, err
}
return id, err
}
func (s *Sqlite) PackagesCreate(ctx context.Context, req *pb.PackagesCreateReq) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
logger.Errorf(ctx, "AddPackage: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
res, err := tx.ExecContext(ctx, queryPackagesCreate, req.Name, req.Url, pq.Array(req.Modules))
if err != nil {
return err
}
if aff, affErr := res.RowsAffected(); err != nil {
err = affErr
} else if aff == 0 {
err = errors.New("rows affected is 0")
}
return err
}
func (s *Sqlite) InsertButchModules(ctx context.Context, req []models.Module) ([]uint64, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
logger.Errorf(ctx, "AddPackage: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
query := generateQuery(req)
rows, err := tx.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer func() {
if err = rows.Close(); err != nil {
return
}
err = rows.Err()
}()
result := make([]uint64, 0)
for rows.Next() {
tmp := uint64(0)
if err = rows.Scan(&tmp); err != nil {
return nil, err
}
result = append(result, tmp)
}
return result, err
}
func (s *Sqlite) GetModule(ctx context.Context, req *pb.ModulesListReq) ([]*models.Module, error) {
var err error
var modules []*models.Module
rows, err := s.db.QueryContext(ctx, queryModulesList)
if err != nil {
return nil, err
}
defer func() {
if err = rows.Close(); err != nil {
return
}
err = rows.Err()
}()
for ; rows.Err() == nil; rows.Next() {
mod := &models.Module{}
if err = rows.Scan(
&mod.ID,
&mod.Name,
&mod.Version,
&mod.LastVersion,
); err != nil {
return nil, err
}
modules = append(modules, mod)
}
if err = rows.Err(); err != nil {
return nil, err
}
if err = rows.Close(); err != nil {
return nil, err
}
return modules, nil
}
func (s *Sqlite) CommentsList(ctx context.Context, req *pb.CommentsListReq) ([]*models.Comment, error) {
var comments []*models.Comment
rows, err := s.db.QueryContext(ctx, queryCommentsList, req.PackageId)
if err != nil {
return nil, err
}
defer func() {
if err = rows.Close(); err != nil {
return
}
err = rows.Err()
}()
for ; rows.Err() == nil; rows.Next() {
com := &models.Comment{}
if err = rows.Scan(
&com.ID,
&com.Text,
&com.Created,
&com.Updated,
); err != nil {
_ = rows.Close()
return nil, err
}
comments = append(comments, com)
}
if err = rows.Err(); err != nil {
return nil, err
}
if err = rows.Close(); err != nil {
return nil, err
}
return comments, nil
}
func convertSliceUInt(arg ...uint64) []interface{} {
result := make([]interface{}, 0, len(arg))
for i := range arg {
result = append(result, arg[i])
}
return result
}
func generateQuery(rsp []models.Module) string {
const pattern = `%c('%s', '%s', '%s')`
build := strings.Builder{}
comma := ' '
for i := range rsp {
str := fmt.Sprintf(pattern, comma, rsp[i].Name, rsp[i].Version, rsp[i].LastVersion)
build.WriteString(str)
comma = ','
}
return fmt.Sprintf(queryInsMsgGetIDs, build.String())
}
func generateArrayIneq(count int) string {
return "(?" + strings.Repeat(",?", count-1) + ")"
}

View File

@@ -0,0 +1,69 @@
package storage
import (
"context"
"database/sql"
"embed"
"errors"
"git.unistack.org/unistack-org/pkgdash/internal/models"
// "git.unistack.org/unistack-org/pkgdash/internal/storage/postgres"
"git.unistack.org/unistack-org/pkgdash/internal/storage/sqlite"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
//go:embed migrations
var fs embed.FS
var storages = map[string]func(*sql.DB, embed.FS) interface{}{
//"postgres": postgres.NewStorage(),
"sqlite": sqlite.NewStorage(),
}
type contextKey string
var storeIdent = contextKey("store")
type Migrate interface {
MigrateUp() error
MigrateDown() error
}
type Storage interface {
Migrate
PackagesCreate(ctx context.Context, req *pb.PackagesCreateReq) error
PackagesList(ctx context.Context, req *pb.PackagesListReq) ([]*models.Package, error)
PackagesUpdate(ctx context.Context, req *pb.PackagesUpdateReq) error
PackagesDelete(ctx context.Context, req *pb.PackagesDeleteReq) error
CommentsCreate(ctx context.Context, req *pb.CommentsCreateReq) (*models.Comment, error)
CommentsDelete(ctx context.Context, req *pb.CommentsDeleteReq) error
CommentsList(ctx context.Context, req *pb.CommentsListReq) ([]*models.Comment, error)
InsertButchModules(ctx context.Context, req []models.Module) ([]uint64, error)
ModulesList(ctx context.Context, req *pb.ModulesListReq) ([]*models.Module, error)
}
func NewStorage(name string, db *sql.DB) (Storage, error) {
function, ok := storages[name]
if !ok {
return nil, errors.New("incorrect name store")
}
store := function(db, fs)
database, ok := store.(Storage)
if !ok {
return nil, errors.New("dont implements interface Storage")
}
return database, nil
}
func InContext(ctx context.Context, val Storage) context.Context {
return context.WithValue(ctx, storeIdent, val)
}
func FromContext(ctx context.Context) (Storage, error) {
if store, ok := ctx.Value(storeIdent).(Storage); !ok {
return nil, errors.New("empty store")
} else {
return store, nil
}
}

View File

@@ -0,0 +1,69 @@
package storage
import (
"context"
"database/sql"
"fmt"
"testing"
"git.unistack.org/unistack-org/pkgdash/internal/storage/sqlite"
pb "git.unistack.org/unistack-org/pkgdash/proto"
)
func TestGetModule(t *testing.T) {
conn, err := sql.Open("sqlite3", "/Users/devstigneev_local/GolandProjects/unistack/pkgdash/identifier.sqlite")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
if err = conn.Ping(); err != nil {
t.Fatal(err)
}
st := sqlite.NewStorage()
store := st(conn, fs)
s, ok := store.(Storage)
if !ok {
t.Fatal("dont implements interface Storage")
}
req := &pb.GetModuleReq{
Id: []uint64{1, 2, 3},
}
module, err := s.GetModule(context.Background(), req)
if err != nil {
t.Fatal(err)
}
fmt.Println(module)
}
func TestGetComment(t *testing.T) {
conn, err := sql.Open("sqlite3", "/Users/devstigneev_local/GolandProjects/unistack/pkgdash/identifier.sqlite")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
if err = conn.Ping(); err != nil {
t.Fatal(err)
}
st := sqlite.NewStorage()
store := st(conn, fs)
s, ok := store.(Storage)
if !ok {
t.Fatal("dont implements interface Storage")
}
req := &pb.GetCommentsReq{
Id: []uint64{1, 2, 3, 15},
}
comments, err := s.GetComment(context.Background(), req)
if err != nil {
t.Fatal(err)
}
fmt.Println(comments.Decode())
}