mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Add project management
This commit is contained in:
1248
pkg/api/generated.go
1248
pkg/api/generated.go
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,14 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type CloseProjectResult struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type DeleteProjectResult struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type HTTPHeader struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
@ -34,6 +42,11 @@ type HTTPResponseLog struct {
|
||||
Headers []HTTPHeader `json:"headers"`
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Name string `json:"name"`
|
||||
IsActive bool `json:"isActive"`
|
||||
}
|
||||
|
||||
type HTTPMethod string
|
||||
|
||||
const (
|
||||
|
@ -7,20 +7,35 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/dstotijn/hetty/pkg/proj"
|
||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
RequestLogService *reqlog.Service
|
||||
ProjectService *proj.Service
|
||||
}
|
||||
|
||||
type queryResolver struct{ *Resolver }
|
||||
type mutationResolver struct{ *Resolver }
|
||||
|
||||
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
||||
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
||||
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog, error) {
|
||||
opts := reqlog.FindRequestsOptions{OmitOutOfScope: false}
|
||||
reqs, err := r.RequestLogService.FindRequests(ctx, opts)
|
||||
if err == reqlog.ErrNoProject {
|
||||
return nil, &gqlerror.Error{
|
||||
Path: graphql.GetPath(ctx),
|
||||
Message: "No active project.",
|
||||
Extensions: map[string]interface{}{
|
||||
"code": "no_active_project",
|
||||
},
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not query repository for requests: %v", err)
|
||||
}
|
||||
@ -116,3 +131,65 @@ func parseRequestLog(req reqlog.Request) (HTTPRequestLog, error) {
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) OpenProject(ctx context.Context, name string) (*Project, error) {
|
||||
p, err := r.ProjectService.Open(name)
|
||||
if err == proj.ErrInvalidName {
|
||||
return nil, gqlerror.Errorf("Project name must only contain alphanumeric or space chars.")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open project: %v", err)
|
||||
}
|
||||
return &Project{
|
||||
Name: p.Name,
|
||||
IsActive: p.IsActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) {
|
||||
p, err := r.ProjectService.ActiveProject()
|
||||
if err == proj.ErrNoProject {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open project: %v", err)
|
||||
}
|
||||
|
||||
return &Project{
|
||||
Name: p.Name,
|
||||
IsActive: p.IsActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
|
||||
p, err := r.ProjectService.Projects()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get projects: %v", err)
|
||||
}
|
||||
|
||||
projects := make([]Project, len(p))
|
||||
for i, proj := range p {
|
||||
projects[i] = Project{
|
||||
Name: proj.Name,
|
||||
IsActive: proj.IsActive,
|
||||
}
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CloseProject(ctx context.Context) (*CloseProjectResult, error) {
|
||||
if err := r.ProjectService.Close(); err != nil {
|
||||
return nil, fmt.Errorf("could not close project: %v", err)
|
||||
}
|
||||
return &CloseProjectResult{true}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteProject(ctx context.Context, name string) (*DeleteProjectResult, error) {
|
||||
if err := r.ProjectService.Delete(name); err != nil {
|
||||
return nil, fmt.Errorf("could not delete project: %v", err)
|
||||
}
|
||||
return &DeleteProjectResult{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
@ -23,9 +23,30 @@ type HttpHeader {
|
||||
value: String!
|
||||
}
|
||||
|
||||
type Project {
|
||||
name: String!
|
||||
isActive: Boolean!
|
||||
}
|
||||
|
||||
type CloseProjectResult {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type DeleteProjectResult {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Query {
|
||||
httpRequestLog(id: ID!): HttpRequestLog
|
||||
httpRequestLogs: [HttpRequestLog!]!
|
||||
activeProject: Project
|
||||
projects: [Project!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
openProject(name: String!): Project
|
||||
closeProject: CloseProjectResult!
|
||||
deleteProject(name: String!): DeleteProjectResult!
|
||||
}
|
||||
|
||||
enum HttpMethod {
|
||||
|
@ -3,11 +3,10 @@ package sqlite
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||
@ -33,13 +32,10 @@ type httpRequestLogsQuery struct {
|
||||
joinResponse bool
|
||||
}
|
||||
|
||||
// New returns a new Client.
|
||||
func New(filename string) (*Client, error) {
|
||||
// Create directory for DB if it doesn't exist yet.
|
||||
if dbDir, _ := filepath.Split(filename); dbDir != "" {
|
||||
if _, err := os.Stat(dbDir); os.IsNotExist(err) {
|
||||
os.Mkdir(dbDir, 0755)
|
||||
}
|
||||
// Open opens a database.
|
||||
func (c *Client) Open(filename string) error {
|
||||
if c.db != nil {
|
||||
return errors.New("sqlite: database already open")
|
||||
}
|
||||
|
||||
opts := make(url.Values)
|
||||
@ -48,24 +44,24 @@ func New(filename string) (*Client, error) {
|
||||
dsn := fmt.Sprintf("file:%v?%v", filename, opts.Encode())
|
||||
db, err := sqlx.Open("sqlite3", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return fmt.Errorf("sqlite: could not open database: %v", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("sqlite: could not ping database: %v", err)
|
||||
return fmt.Errorf("sqlite: could not ping database: %v", err)
|
||||
}
|
||||
|
||||
c := &Client{db: db}
|
||||
|
||||
if err := c.prepareSchema(); err != nil {
|
||||
return nil, fmt.Errorf("sqlite: could not prepare schema: %v", err)
|
||||
if err := prepareSchema(db); err != nil {
|
||||
return fmt.Errorf("sqlite: could not prepare schema: %v", err)
|
||||
}
|
||||
|
||||
return &Client{db: db}, nil
|
||||
c.db = db
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) prepareSchema() error {
|
||||
_, err := c.db.Exec(`CREATE TABLE IF NOT EXISTS http_requests (
|
||||
func prepareSchema(db *sqlx.DB) error {
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS http_requests (
|
||||
id INTEGER PRIMARY KEY,
|
||||
proto TEXT,
|
||||
url TEXT,
|
||||
@ -77,7 +73,7 @@ func (c Client) prepareSchema() error {
|
||||
return fmt.Errorf("could not create http_requests table: %v", err)
|
||||
}
|
||||
|
||||
_, err = c.db.Exec(`CREATE TABLE IF NOT EXISTS http_responses (
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS http_responses (
|
||||
id INTEGER PRIMARY KEY,
|
||||
req_id INTEGER REFERENCES http_requests(id) ON DELETE CASCADE,
|
||||
proto TEXT,
|
||||
@ -90,7 +86,7 @@ func (c Client) prepareSchema() error {
|
||||
return fmt.Errorf("could not create http_responses table: %v", err)
|
||||
}
|
||||
|
||||
_, err = c.db.Exec(`CREATE TABLE IF NOT EXISTS http_headers (
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS http_headers (
|
||||
id INTEGER PRIMARY KEY,
|
||||
req_id INTEGER REFERENCES http_requests(id) ON DELETE CASCADE,
|
||||
res_id INTEGER REFERENCES http_responses(id) ON DELETE CASCADE,
|
||||
@ -104,9 +100,16 @@ func (c Client) prepareSchema() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close uses the underlying database.
|
||||
// Close uses the underlying database if it's open.
|
||||
func (c *Client) Close() error {
|
||||
return c.db.Close()
|
||||
if c.db == nil {
|
||||
return nil
|
||||
}
|
||||
if err := c.db.Close(); err != nil {
|
||||
return fmt.Errorf("sqlite: could not close database: %v", err)
|
||||
}
|
||||
c.db = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var reqFieldToColumnMap = map[string]string{
|
||||
@ -136,6 +139,10 @@ func (c *Client) FindRequestLogs(
|
||||
opts reqlog.FindRequestsOptions,
|
||||
scope *scope.Scope,
|
||||
) (reqLogs []reqlog.Request, err error) {
|
||||
if c.db == nil {
|
||||
return nil, reqlog.ErrNoProject
|
||||
}
|
||||
|
||||
httpReqLogsQuery := parseHTTPRequestLogsQuery(ctx)
|
||||
|
||||
reqQuery := sq.
|
||||
@ -178,6 +185,9 @@ func (c *Client) FindRequestLogs(
|
||||
}
|
||||
|
||||
func (c *Client) FindRequestLogByID(ctx context.Context, id int64) (reqlog.Request, error) {
|
||||
if c.db == nil {
|
||||
return reqlog.Request{}, reqlog.ErrNoProject
|
||||
}
|
||||
httpReqLogsQuery := parseHTTPRequestLogsQuery(ctx)
|
||||
|
||||
reqQuery := sq.
|
||||
@ -218,6 +228,9 @@ func (c *Client) AddRequestLog(
|
||||
body []byte,
|
||||
timestamp time.Time,
|
||||
) (*reqlog.Request, error) {
|
||||
if c.db == nil {
|
||||
return nil, reqlog.ErrNoProject
|
||||
}
|
||||
|
||||
reqLog := &reqlog.Request{
|
||||
Request: req,
|
||||
@ -289,6 +302,10 @@ func (c *Client) AddResponseLog(
|
||||
body []byte,
|
||||
timestamp time.Time,
|
||||
) (*reqlog.Response, error) {
|
||||
if c.db == nil {
|
||||
return nil, reqlog.ErrNoProject
|
||||
}
|
||||
|
||||
resLog := &reqlog.Response{
|
||||
RequestID: reqID,
|
||||
Response: res,
|
||||
@ -495,3 +512,7 @@ func (c *Client) queryHeaders(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) IsOpen() bool {
|
||||
return c.db != nil
|
||||
}
|
||||
|
135
pkg/proj/proj.go
Normal file
135
pkg/proj/proj.go
Normal file
@ -0,0 +1,135 @@
|
||||
package proj
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/db/sqlite"
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
)
|
||||
|
||||
// Service is used for managing projects.
|
||||
type Service struct {
|
||||
dbPath string
|
||||
db *sqlite.Client
|
||||
name string
|
||||
|
||||
Scope *scope.Scope
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Name string
|
||||
IsActive bool
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoProject = errors.New("proj: no open project")
|
||||
ErrInvalidName = errors.New("proj: invalid name, must be alphanumeric or whitespace chars")
|
||||
)
|
||||
|
||||
var nameRegexp = regexp.MustCompile(`^[\w\d\s]+$`)
|
||||
|
||||
// NewService returns a new Service.
|
||||
func NewService(dbPath string) (*Service, error) {
|
||||
// Create directory for DBs if it doesn't exist yet.
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dbPath, 0755); err != nil {
|
||||
return nil, fmt.Errorf("proj: could not create project directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &Service{
|
||||
dbPath: dbPath,
|
||||
db: &sqlite.Client{},
|
||||
Scope: scope.New(nil),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the currently open project database (if there is one).
|
||||
func (svc *Service) Close() error {
|
||||
if err := svc.db.Close(); err != nil {
|
||||
return fmt.Errorf("proj: could not close project: %v", err)
|
||||
}
|
||||
svc.name = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a project database file from disk (if there is one).
|
||||
func (svc *Service) Delete(name string) error {
|
||||
if name == "" {
|
||||
return errors.New("proj: name cannot be empty")
|
||||
}
|
||||
if svc.name == name {
|
||||
return fmt.Errorf("proj: project (%v) is active", name)
|
||||
}
|
||||
|
||||
if err := os.Remove(filepath.Join(svc.dbPath, name+".db")); err != nil {
|
||||
return fmt.Errorf("proj: could not remove database file: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Database returns the currently open database. If no database is open, it will
|
||||
// return `nil`.
|
||||
func (svc *Service) Database() *sqlite.Client {
|
||||
return svc.db
|
||||
}
|
||||
|
||||
// Open opens a database identified with `name`. If a database with this
|
||||
// identifier doesn't exist yet, it will be automatically created.
|
||||
func (svc *Service) Open(name string) (Project, error) {
|
||||
if !nameRegexp.MatchString(name) {
|
||||
return Project{}, ErrInvalidName
|
||||
}
|
||||
if err := svc.db.Close(); err != nil {
|
||||
return Project{}, fmt.Errorf("proj: could not close previously open database: %v", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(svc.dbPath, name+".db")
|
||||
|
||||
err := svc.db.Open(dbPath)
|
||||
if err != nil {
|
||||
return Project{}, fmt.Errorf("proj: could not open database: %v", err)
|
||||
}
|
||||
|
||||
svc.name = name
|
||||
|
||||
return Project{
|
||||
Name: name,
|
||||
IsActive: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ActiveProject() (Project, error) {
|
||||
if !svc.db.IsOpen() {
|
||||
return Project{}, ErrNoProject
|
||||
}
|
||||
|
||||
return Project{
|
||||
Name: svc.name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) Projects() ([]Project, error) {
|
||||
files, err := ioutil.ReadDir(svc.dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proj: could not read projects directory: %v", err)
|
||||
}
|
||||
|
||||
projects := make([]Project, len(files))
|
||||
for i, file := range files {
|
||||
projName := strings.TrimSuffix(file.Name(), ".db")
|
||||
projects[i] = Project{
|
||||
Name: projName,
|
||||
IsActive: svc.name == projName,
|
||||
}
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
@ -83,13 +83,13 @@ func LoadOrCreateCA(caKeyFile, caCertFile string) (*x509.Certificate, *rsa.Priva
|
||||
keyDir, _ := filepath.Split(caKeyFile)
|
||||
if keyDir != "" {
|
||||
if _, err := os.Stat(keyDir); os.IsNotExist(err) {
|
||||
os.Mkdir(keyDir, 0755)
|
||||
os.MkdirAll(keyDir, 0755)
|
||||
}
|
||||
}
|
||||
keyDir, _ = filepath.Split(caCertFile)
|
||||
if keyDir != "" {
|
||||
if _, err := os.Stat("keyDir"); os.IsNotExist(err) {
|
||||
os.Mkdir(keyDir, 0755)
|
||||
os.MkdirAll(keyDir, 0755)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,10 @@ import (
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
)
|
||||
|
||||
type RepositoryProvider interface {
|
||||
Repository() Repository
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
FindRequestLogs(ctx context.Context, opts FindRequestsOptions, scope *scope.Scope) ([]Request, error)
|
||||
FindRequestLogByID(ctx context.Context, id int64) (Request, error)
|
||||
|
@ -19,7 +19,10 @@ type contextKey int
|
||||
|
||||
const LogBypassedKey contextKey = 0
|
||||
|
||||
var ErrRequestNotFound = errors.New("reqlog: request not found")
|
||||
var (
|
||||
ErrRequestNotFound = errors.New("reqlog: request not found")
|
||||
ErrNoProject = errors.New("reqlog: no project")
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
ID int64
|
||||
@ -133,6 +136,11 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
||||
}
|
||||
|
||||
reqLog, err := svc.addRequest(req.Context(), *clone, body, now)
|
||||
if err == ErrNoProject {
|
||||
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
||||
*req = *req.WithContext(ctx)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Could not store request log: %v", err)
|
||||
return
|
||||
|
Reference in New Issue
Block a user