Add twirp RPC backend API for lure-web
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
19ced9795c
commit
6cd0802f64
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/lure
|
||||
/lure-api
|
||||
/dist/
|
||||
/internal/config/version.txt
|
157
cmd/lure-api/api.go
Normal file
157
cmd/lure-api/api.go
Normal file
@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/genjidb/genji"
|
||||
"github.com/genjidb/genji/document"
|
||||
"github.com/genjidb/genji/types"
|
||||
"go.arsenm.dev/lure/cmd/lure-api/internal/api"
|
||||
"go.arsenm.dev/lure/internal/db"
|
||||
)
|
||||
|
||||
type lureWebAPI struct {
|
||||
db *genji.DB
|
||||
}
|
||||
|
||||
func (l lureWebAPI) CreateComment(ctx context.Context, req *api.CreateCommentRequest) (*api.CreateCommentResponse, error) {
|
||||
count, err := db.CountComments(l.db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.InsertComment(l.db, db.Comment{
|
||||
CommentID: count,
|
||||
PackageName: req.PkgName,
|
||||
PackageRepo: req.Repository,
|
||||
TimeCreated: time.Now().Unix(),
|
||||
Contents: req.Contents,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.CreateCommentResponse{CommentId: count}, nil
|
||||
}
|
||||
|
||||
func (l lureWebAPI) EditComment(ctx context.Context, req *api.EditCommentRequest) (*api.EmptyResponse, error) {
|
||||
doc, err := db.GetComment(l.db, "comment_id = ?", req.CommentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var comment db.Comment
|
||||
err = document.ScanDocument(doc, &comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
comment.Contents = req.NewContents
|
||||
|
||||
err = db.InsertComment(l.db, comment)
|
||||
return &api.EmptyResponse{}, err
|
||||
}
|
||||
|
||||
func (l lureWebAPI) GetComments(ctx context.Context, req *api.GetCommentsRequest) (*api.GetCommentsResponse, error) {
|
||||
doc, err := db.GetComments(
|
||||
l.db,
|
||||
"package_repo = ? AND package_name = ? AND time_created >= ? LIMIT ?",
|
||||
req.PkgName,
|
||||
req.Repository,
|
||||
req.CreatedSince,
|
||||
req.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &api.GetCommentsResponse{}
|
||||
err = doc.Iterate(func(d types.Document) error {
|
||||
comment := &api.Comment{}
|
||||
err = document.ScanDocument(d, comment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out.Comments = append(out.Comments, comment)
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (l lureWebAPI) Search(ctx context.Context, req *api.SearchRequest) (*api.SearchResponse, error) {
|
||||
query := "(name LIKE ? OR description LIKE ? OR ? IN provides)"
|
||||
args := []any{"%" + req.Query + "%", "%" + req.Query + "%", req.Query}
|
||||
|
||||
if req.FilterValue != nil && req.FilterType != api.FILTER_TYPE_NO_FILTER {
|
||||
switch req.FilterType {
|
||||
case api.FILTER_TYPE_IN_REPOSITORY:
|
||||
query += " AND repository = ?"
|
||||
case api.FILTER_TYPE_SUPPORTS_ARCH:
|
||||
query += " AND ? IN architectures"
|
||||
}
|
||||
args = append(args, *req.FilterValue)
|
||||
}
|
||||
|
||||
if req.SortBy != api.SORT_BY_UNSORTED {
|
||||
switch req.SortBy {
|
||||
case api.SORT_BY_NAME:
|
||||
query += " ORDER BY name"
|
||||
case api.SORT_BY_REPOSITORY:
|
||||
query += " ORDER BY repository"
|
||||
case api.SORT_BY_VERSION:
|
||||
query += " ORDER BY version"
|
||||
}
|
||||
}
|
||||
|
||||
if req.Limit != 0 {
|
||||
query += " LIMIT " + strconv.FormatInt(req.Limit, 10)
|
||||
}
|
||||
|
||||
doc, err := db.GetPkgs(l.db, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Println(query, args)
|
||||
out := &api.SearchResponse{}
|
||||
err = doc.Iterate(func(d types.Document) error {
|
||||
pkg := &db.Package{}
|
||||
err = document.ScanDocument(d, pkg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out.Packages = append(out.Packages, &api.Package{
|
||||
Name: pkg.Name,
|
||||
Repository: pkg.Repository,
|
||||
Version: pkg.Version,
|
||||
Release: int64(pkg.Release),
|
||||
Epoch: ptr(int64(pkg.Epoch)),
|
||||
Description: &pkg.Description,
|
||||
Homepage: &pkg.Homepage,
|
||||
Maintainer: &pkg.Maintainer,
|
||||
Architectures: pkg.Architectures,
|
||||
Licenses: pkg.Licenses,
|
||||
Provides: pkg.Provides,
|
||||
Conflicts: pkg.Conflicts,
|
||||
Replaces: pkg.Replaces,
|
||||
Depends: dbMapToAPI(pkg.Depends),
|
||||
BuildDepends: dbMapToAPI(pkg.BuildDepends),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func dbMapToAPI(m map[string][]string) map[string]*api.StringList {
|
||||
out := make(map[string]*api.StringList, len(m))
|
||||
for override, list := range m {
|
||||
out[override] = &api.StringList{Entries: list}
|
||||
}
|
||||
return out
|
||||
}
|
23
cmd/lure-api/db.go
Normal file
23
cmd/lure-api/db.go
Normal file
@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/genjidb/genji"
|
||||
"go.arsenm.dev/logger/log"
|
||||
"go.arsenm.dev/lure/internal/config"
|
||||
"go.arsenm.dev/lure/internal/db"
|
||||
)
|
||||
|
||||
var gdb *genji.DB
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
gdb, err = genji.Open(config.DBPath)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening database").Err(err).Send()
|
||||
}
|
||||
|
||||
err = db.Init(gdb)
|
||||
if err != nil {
|
||||
log.Fatal("Error initializing database").Err(err).Send()
|
||||
}
|
||||
}
|
1202
cmd/lure-api/internal/api/lure.pb.go
Normal file
1202
cmd/lure-api/internal/api/lure.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
1993
cmd/lure-api/internal/api/lure.twirp.go
Normal file
1993
cmd/lure-api/internal/api/lure.twirp.go
Normal file
File diff suppressed because it is too large
Load Diff
109
cmd/lure-api/lure.proto
Normal file
109
cmd/lure-api/lure.proto
Normal file
@ -0,0 +1,109 @@
|
||||
syntax = "proto3";
|
||||
package lure;
|
||||
option go_package = "/internal/api";
|
||||
|
||||
// EmptyResponse is an empty API response
|
||||
message EmptyResponse {}
|
||||
|
||||
message Comment {
|
||||
int64 comment_id = 1;
|
||||
int64 time_created = 2;
|
||||
string contents = 3;
|
||||
}
|
||||
|
||||
// CreateCommentRequest is a request to create a comment
|
||||
message CreateCommentRequest {
|
||||
string repository = 1;
|
||||
string pkg_name = 2;
|
||||
string contents = 3;
|
||||
}
|
||||
|
||||
// CreateCommentResponse is a response to CreateCommentRequest
|
||||
message CreateCommentResponse {
|
||||
int64 comment_id = 1;
|
||||
}
|
||||
|
||||
// EditCommentRequest is a request to edit a comment
|
||||
message EditCommentRequest {
|
||||
int64 comment_id = 1;
|
||||
string new_contents = 2;
|
||||
}
|
||||
|
||||
// EditCommentRequest is a request to get comments on a package
|
||||
message GetCommentsRequest {
|
||||
string repository = 1;
|
||||
string pkg_name = 2;
|
||||
int64 created_since = 3;
|
||||
int64 limit = 4;
|
||||
}
|
||||
|
||||
// EditCommentRequest is a response to GetCommentsRequest
|
||||
message GetCommentsResponse {
|
||||
repeated Comment comments = 1;
|
||||
}
|
||||
|
||||
// SORT_BY represents possible things to sort packages by
|
||||
enum SORT_BY {
|
||||
UNSORTED = 0;
|
||||
NAME = 1;
|
||||
REPOSITORY = 2;
|
||||
VERSION = 3;
|
||||
}
|
||||
|
||||
// FILTER_TYPE represents possible filters for packages
|
||||
enum FILTER_TYPE {
|
||||
NO_FILTER = 0;
|
||||
IN_REPOSITORY = 1;
|
||||
SUPPORTS_ARCH = 2;
|
||||
}
|
||||
|
||||
// SearchRequest is a request to search for packages
|
||||
message SearchRequest {
|
||||
string query = 1;
|
||||
int64 limit = 2;
|
||||
SORT_BY sort_by = 3;
|
||||
FILTER_TYPE filter_type = 4;
|
||||
optional string filter_value = 5;
|
||||
}
|
||||
|
||||
// StringList contains a list of strings
|
||||
message StringList {
|
||||
repeated string entries = 1;
|
||||
}
|
||||
|
||||
// Package represents a LURE package
|
||||
message Package {
|
||||
string name = 1;
|
||||
string repository = 2;
|
||||
string version = 3;
|
||||
int64 release = 4;
|
||||
optional int64 epoch = 5;
|
||||
optional string description = 6;
|
||||
optional string homepage = 7;
|
||||
optional string maintainer = 8;
|
||||
repeated string architectures = 9;
|
||||
repeated string licenses = 10;
|
||||
repeated string provides = 11;
|
||||
repeated string conflicts = 12;
|
||||
repeated string replaces = 13;
|
||||
map<string, StringList> depends = 14;
|
||||
map<string, StringList> build_depends = 15;
|
||||
}
|
||||
|
||||
// SearchResponse contains returned packages
|
||||
message SearchResponse {
|
||||
repeated Package packages = 1;
|
||||
}
|
||||
|
||||
// Web is the LURE Web service
|
||||
service API {
|
||||
// CreateComment creates a new comment on the given package
|
||||
rpc CreateComment(CreateCommentRequest) returns (CreateCommentResponse);
|
||||
// EditComment edits an existing comment
|
||||
rpc EditComment(EditCommentRequest) returns (EmptyResponse);
|
||||
// GetComments returns the comments on a particular package
|
||||
rpc GetComments(GetCommentsRequest) returns (GetCommentsResponse);
|
||||
|
||||
// Search searches through LURE packages in the database
|
||||
rpc Search(SearchRequest) returns (SearchResponse);
|
||||
}
|
53
cmd/lure-api/main.go
Normal file
53
cmd/lure-api/main.go
Normal file
@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/twitchtv/twirp"
|
||||
"go.arsenm.dev/logger"
|
||||
"go.arsenm.dev/logger/log"
|
||||
"go.arsenm.dev/lure/cmd/lure-api/internal/api"
|
||||
)
|
||||
|
||||
//go:generate protoc --twirp_out=. lure.proto
|
||||
//go:generate protoc --go_out=. lure.proto
|
||||
|
||||
func init() {
|
||||
log.Logger = logger.NewPretty(os.Stderr)
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := flag.String("a", ":8080", "Listen address for API server")
|
||||
logFile := flag.String("l", "", "Output file for JSON log")
|
||||
flag.Parse()
|
||||
|
||||
if *logFile != "" {
|
||||
fl, err := os.Create(*logFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error creating log file").Err(err).Send()
|
||||
}
|
||||
defer fl.Close()
|
||||
|
||||
log.Logger = logger.NewMulti(log.Logger, logger.NewJSON(fl))
|
||||
}
|
||||
|
||||
srv := api.NewAPIServer(
|
||||
lureWebAPI{db: gdb},
|
||||
twirp.WithServerPathPrefix(""),
|
||||
)
|
||||
|
||||
ln, err := net.Listen("tcp", *addr)
|
||||
if err != nil {
|
||||
log.Fatal("Error starting listener").Err(err).Send()
|
||||
}
|
||||
|
||||
log.Info("Starting HTTP API server").Str("addr", ln.Addr().String()).Send()
|
||||
|
||||
err = http.Serve(ln, srv)
|
||||
if err != nil {
|
||||
log.Fatal("Error while running server").Err(err).Send()
|
||||
}
|
||||
}
|
3
go.mod
3
go.mod
@ -18,10 +18,12 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/pelletier/go-toml/v2 v2.0.5
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible
|
||||
github.com/urfave/cli/v2 v2.16.3
|
||||
go.arsenm.dev/logger v0.0.0-20221007032343-cbffce4f4334
|
||||
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
mvdan.cc/sh/v3 v3.5.1
|
||||
)
|
||||
@ -108,6 +110,5 @@ require (
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
2
go.sum
2
go.sum
@ -550,6 +550,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
|
||||
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
|
@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"github.com/genjidb/genji"
|
||||
"github.com/genjidb/genji/types"
|
||||
)
|
||||
|
||||
// Package is a LURE package's database representation
|
||||
@ -23,6 +24,15 @@ type Package struct {
|
||||
Repository string
|
||||
}
|
||||
|
||||
// Package is a LURE Web comment's database representation
|
||||
type Comment struct {
|
||||
CommentID int64
|
||||
PackageName string
|
||||
PackageRepo string
|
||||
TimeCreated int64
|
||||
Contents string
|
||||
}
|
||||
|
||||
// Init initializes the database
|
||||
func Init(db *genji.DB) error {
|
||||
return db.Exec(`
|
||||
@ -44,6 +54,16 @@ func Init(db *genji.DB) error {
|
||||
builddepends (...),
|
||||
UNIQUE(name, repository)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
comment_id INT PRIMARY KEY,
|
||||
package_name TEXT NOT NULL,
|
||||
package_repo TEXT NOT NULL,
|
||||
time_created INT NOT NULL,
|
||||
contents TEXT NOT NULL,
|
||||
UNIQUE(comment_id),
|
||||
UNIQUE(package_name, package_repo)
|
||||
);
|
||||
`)
|
||||
}
|
||||
|
||||
@ -65,3 +85,42 @@ func GetPkgs(db *genji.DB, where string, args ...any) (*genji.Result, error) {
|
||||
func DeletePkgs(db *genji.DB, where string, args ...any) error {
|
||||
return db.Exec("DELETE FROM pkgs WHERE "+where, args...)
|
||||
}
|
||||
|
||||
func InsertComment(db *genji.DB, c Comment) error {
|
||||
return db.Exec("INSERT INTO comments VALUES ? ON CONFLICT DO REPLACE;", c)
|
||||
}
|
||||
|
||||
func CountComments(db *genji.DB) (int64, error) {
|
||||
doc, err := db.QueryDocument("SELECT count(*) FROM comments;")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
val, err := doc.GetByField("COUNT(*)")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return val.V().(int64), nil
|
||||
}
|
||||
|
||||
// GetComments returns a result containing comments that match the where conditions
|
||||
func GetComments(db *genji.DB, where string, args ...any) (*genji.Result, error) {
|
||||
stream, err := db.Query("SELECT * FROM comments WHERE "+where, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
// GetComment returns a comment that matches the where conditions
|
||||
func GetComment(db *genji.DB, where string, args ...any) (types.Document, error) {
|
||||
doc, err := db.QueryDocument("SELECT * FROM comments WHERE "+where+" LIMIT 1", args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// DeleteComments deletes all comments matching the where conditions
|
||||
func DeleteComments(db *genji.DB, where string, args ...any) error {
|
||||
return db.Exec("DELETE FROM comments WHERE "+where, args...)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user