feat: updates

This commit is contained in:
Jon Lundy 2022-09-04 08:34:22 -06:00
parent 8c54eefcdd
commit f65e9ca666
Signed by untrusted user who does not match committer: xuu
GPG Key ID: C63E6D61F3035024
10 changed files with 523 additions and 75 deletions

View File

@ -15,6 +15,7 @@ import (
"github.com/sour-is/ev/app/salty" "github.com/sour-is/ev/app/salty"
"github.com/sour-is/ev/internal/graph/generated" "github.com/sour-is/ev/internal/graph/generated"
"github.com/sour-is/ev/internal/logz" "github.com/sour-is/ev/internal/logz"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/gql" "github.com/sour-is/ev/pkg/gql"
"github.com/vektah/gqlparser/v2/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
) )
@ -22,6 +23,7 @@ import (
type Resolver struct { type Resolver struct {
msgbus.MsgbusResolver msgbus.MsgbusResolver
salty.SaltyResolver salty.SaltyResolver
es.EventResolver
} }
func New(ctx context.Context, resolvers ...interface{ RegisterHTTP(*http.ServeMux) }) (*Resolver, error) { func New(ctx context.Context, resolvers ...interface{ RegisterHTTP(*http.ServeMux) }) (*Resolver, error) {
@ -43,13 +45,12 @@ outer:
if field.IsNil() && rs.Type().Implements(field.Type()) { if field.IsNil() && rs.Type().Implements(field.Type()) {
span.AddEvent(fmt.Sprint("found ", field.Type().Name())) span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
field.Set(rs) field.Set(rs)
break outer continue outer
} }
} }
span.AddEvent(fmt.Sprint("default ", field.Type().Name())) span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
field.Set(noop) field.Set(noop)
} }
return r, nil return r, nil
@ -107,6 +108,7 @@ func NoopRecover(ctx context.Context, err interface{}) error {
var _ msgbus.MsgbusResolver = (*noop)(nil) var _ msgbus.MsgbusResolver = (*noop)(nil)
var _ salty.SaltyResolver = (*noop)(nil) var _ salty.SaltyResolver = (*noop)(nil)
var _ es.EventResolver = (*noop)(nil)
func (*noop) CreateSaltyUser(ctx context.Context, nick string, pubkey string) (*salty.SaltyUser, error) { func (*noop) CreateSaltyUser(ctx context.Context, nick string, pubkey string) (*salty.SaltyUser, error) {
panic("not implemented") panic("not implemented")
@ -120,4 +122,11 @@ func (*noop) SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, erro
func (*noop) PostAdded(ctx context.Context, streamID string, after int64) (<-chan *msgbus.PostEvent, error) { func (*noop) PostAdded(ctx context.Context, streamID string, after int64) (<-chan *msgbus.PostEvent, error) {
panic("not implemented") panic("not implemented")
} }
func (*noop) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
panic("not implemented")
}
func (*noop) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *es.GQLEvent, error) {
panic("not implemented")
}
func (*noop) RegisterHTTP(*http.ServeMux) {} func (*noop) RegisterHTTP(*http.ServeMux) {}

View File

@ -435,14 +435,21 @@ func (e *PostEvent) SetEventMeta(eventMeta event.Meta) {
} }
e.eventMeta = eventMeta e.eventMeta = eventMeta
} }
func (e *PostEvent) MarshalBinary() ([]byte, error) { func (e *PostEvent) Values() any {
j := struct { if e == nil {
return nil
}
return struct {
Payload []byte Payload []byte
Tags []string Tags []string
}{ }{
Payload: e.payload, Payload: e.payload,
Tags: e.tags, Tags: e.tags,
} }
}
func (e *PostEvent) MarshalBinary() ([]byte, error) {
j := e.Values()
return json.Marshal(&j) return json.Marshal(&j)
} }
func (e *PostEvent) UnmarshalBinary(b []byte) error { func (e *PostEvent) UnmarshalBinary(b []byte) error {

142
app/salty/salty-addr.go Normal file
View File

@ -0,0 +1,142 @@
package salty
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/keys-pub/keys"
"github.com/sour-is/ev/internal/logz"
)
// Config represents a Salty Config for a User which at a minimum is required
// to have an Endpoint and Key (Public Key)
type Config struct {
Endpoint string `json:"endpoint"`
Key string `json:"key"`
}
type Capabilities struct {
AcceptEncoding string
}
func (c Capabilities) String() string {
return fmt.Sprint("accept-encoding: ", c.AcceptEncoding)
}
type Addr struct {
User string
Domain string
capabilities Capabilities
discoveredDomain string
dns DNSResolver
endpoint *url.URL
key *keys.EdX25519PublicKey
}
// ParseAddr parsers a Salty Address for a user into it's user and domain
// parts and returns an Addr object with the User and Domain and a method
// for returning the expected User's Well-Known URI
func (s *service) ParseAddr(addr string) (*Addr, error) {
parts := strings.Split(strings.ToLower(addr), "@")
if len(parts) != 2 {
return nil, fmt.Errorf("expected nick@domain found %q", addr)
}
return &Addr{User: parts[0], Domain: parts[1], dns: s.dns}, nil
}
func (a *Addr) String() string {
return fmt.Sprintf("%s@%s", a.User, a.Domain)
}
// Hash returns the Hex(SHA256Sum()) of the Address
func (a *Addr) Hash() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.ToLower(a.String()))))
}
// URI returns the Well-Known URI for this Addr
func (a *Addr) URI() string {
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.User)
}
// HashURI returns the Well-Known HashURI for this Addr
func (a *Addr) HashURI() string {
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.Hash())
}
// DiscoveredDomain returns the discovered domain (if any) of fallbacks to the Domain
func (a *Addr) DiscoveredDomain() string {
if a.discoveredDomain != "" {
return a.discoveredDomain
}
return a.Domain
}
func (a *Addr) Refresh(ctx context.Context) error {
ctx, span := logz.Span(ctx)
defer span.End()
span.AddEvent(fmt.Sprintf("Looking up SRV record for _salty._tcp.%s", a.Domain))
if target, _, err := a.dns.LookupSRV(ctx, "salty", "tcp", a.Domain); err == nil {
a.discoveredDomain = target
span.AddEvent(fmt.Sprintf("Discovered salty services %s", a.discoveredDomain))
} else if err != nil {
span.AddEvent(fmt.Sprintf("error looking up SRV record for _salty._tcp.%s : %s", a.Domain, err))
}
config, cap, err := fetchConfig(ctx, a.HashURI())
if err != nil {
// Fallback to plain user nick
config, cap, err = fetchConfig(ctx, a.URI())
}
if err != nil {
return fmt.Errorf("error looking up user %s: %w", a, err)
}
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(config.Key))
if err != nil {
return fmt.Errorf("error parsing public key %s: %w", config.Key, err)
}
a.key = key
u, err := url.Parse(config.Endpoint)
if err != nil {
return fmt.Errorf("error parsing endpoint %s: %w", config.Endpoint, err)
}
a.endpoint = u
a.capabilities = cap
span.AddEvent(fmt.Sprintf("Discovered endpoint: %v", a.endpoint))
span.AddEvent(fmt.Sprintf("Discovered capability: %v", a.capabilities))
return nil
}
func fetchConfig(ctx context.Context, addr string) (config Config, cap Capabilities, err error) {
ctx, span := logz.Span(ctx)
defer span.End()
var req *http.Request
req, err = http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
if err != nil {
return
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return
}
if err = json.NewDecoder(res.Body).Decode(&config); err != nil {
return
}
cap.AcceptEncoding = res.Header.Get("Accept-Encoding")
return
}

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net"
"net/http" "net/http"
"path" "path"
"strings" "strings"
@ -19,12 +20,21 @@ import (
"go.uber.org/multierr" "go.uber.org/multierr"
) )
type DNSResolver interface {
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
}
type service struct { type service struct {
baseURL string baseURL string
es *es.EventStore es *es.EventStore
dns DNSResolver
Mresolver_create_salty_user syncint64.Counter m_create_user syncint64.Counter
Mresolver_salty_user syncint64.Counter m_get_user syncint64.Counter
m_api_ping syncint64.Counter
m_api_register syncint64.Counter
m_api_lookup syncint64.Counter
m_api_send syncint64.Counter
} }
type contextKey struct { type contextKey struct {
name string name string
@ -51,13 +61,25 @@ func New(ctx context.Context, es *es.EventStore, baseURL string) (*service, erro
m := logz.Meter(ctx) m := logz.Meter(ctx)
svc := &service{baseURL: baseURL, es: es} svc := &service{baseURL: baseURL, es: es, dns: net.DefaultResolver}
var err, errs error var err, errs error
svc.Mresolver_create_salty_user, err = m.SyncInt64().Counter("resolver_create_salty_user") svc.m_create_user, err = m.SyncInt64().Counter("salty_create_user")
errs = multierr.Append(errs, err) errs = multierr.Append(errs, err)
svc.Mresolver_salty_user, err = m.SyncInt64().Counter("resolver_salty_user") svc.m_get_user, err = m.SyncInt64().Counter("salty_get_user")
errs = multierr.Append(errs, err)
svc.m_api_ping, err = m.SyncInt64().Counter("salty_api_ping")
errs = multierr.Append(errs, err)
svc.m_api_register, err = m.SyncInt64().Counter("salty_api_register")
errs = multierr.Append(errs, err)
svc.m_api_lookup, err = m.SyncInt64().Counter("salty_api_lookup")
errs = multierr.Append(errs, err)
svc.m_api_send, err = m.SyncInt64().Counter("salty_api_send")
errs = multierr.Append(errs, err) errs = multierr.Append(errs, err)
span.RecordError(err) span.RecordError(err)
@ -73,6 +95,12 @@ func (s *service) BaseURL() string {
func (s *service) RegisterHTTP(mux *http.ServeMux) { func (s *service) RegisterHTTP(mux *http.ServeMux) {
mux.Handle("/.well-known/salty/", logz.Htrace(s, "lookup")) mux.Handle("/.well-known/salty/", logz.Htrace(s, "lookup"))
} }
func (s *service) RegisterAPIv1(mux *http.ServeMux) {
mux.HandleFunc("/ping", s.apiv1)
mux.HandleFunc("/register", s.apiv1)
mux.HandleFunc("/lookup/", s.apiv1)
mux.HandleFunc("/send", s.apiv1)
}
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
ctx, span := logz.Span(ctx) ctx, span := logz.Span(ctx)
@ -112,7 +140,7 @@ func (s *service) CreateSaltyUser(ctx context.Context, nick string, pub string)
ctx, span := logz.Span(ctx) ctx, span := logz.Span(ctx)
defer span.End() defer span.End()
s.Mresolver_create_salty_user.Add(ctx, 1) s.m_create_user.Add(ctx, 1)
streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick)))) streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick))))
span.AddEvent(streamID) span.AddEvent(streamID)
@ -142,7 +170,7 @@ func (s *service) SaltyUser(ctx context.Context, nick string) (*SaltyUser, error
ctx, span := logz.Span(ctx) ctx, span := logz.Span(ctx)
defer span.End() defer span.End()
s.Mresolver_salty_user.Add(ctx, 1) s.m_get_user.Add(ctx, 1)
streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick)))) streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick))))
span.AddEvent(streamID) span.AddEvent(streamID)
@ -168,3 +196,54 @@ func (s *service) GetMiddleware() func(http.Handler) http.Handler {
}) })
} }
} }
func (s *service) apiv1(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx, span := logz.Span(ctx)
defer span.End()
switch r.Method {
case http.MethodGet:
switch {
case r.URL.Path == "/ping":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{}`))
case strings.HasPrefix(r.URL.Path, "/lookup/"):
addr, err := s.ParseAddr(strings.TrimPrefix(r.URL.Path, "/lookup/"))
if err != nil {
span.RecordError(err)
w.WriteHeader(http.StatusBadRequest)
return
}
err = addr.Refresh(ctx)
if err != nil {
span.RecordError(err)
w.WriteHeader(http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(addr)
return
default:
w.WriteHeader(http.StatusNotFound)
return
}
case http.MethodPost:
switch r.URL.Path {
case "/register":
case "/send":
default:
w.WriteHeader(http.StatusNotFound)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}

View File

@ -18,7 +18,7 @@ import (
"github.com/99designs/gqlgen/plugin/federation/fedruntime" "github.com/99designs/gqlgen/plugin/federation/fedruntime"
"github.com/sour-is/ev/app/msgbus" "github.com/sour-is/ev/app/msgbus"
"github.com/sour-is/ev/app/salty" "github.com/sour-is/ev/app/salty"
"github.com/sour-is/ev/internal/graph/model" "github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/gql" "github.com/sour-is/ev/pkg/gql"
gqlparser "github.com/vektah/gqlparser/v2" gqlparser "github.com/vektah/gqlparser/v2"
@ -58,10 +58,11 @@ type ComplexityRoot struct {
} }
Event struct { Event struct {
EventID func(childComplexity int) int Bytes func(childComplexity int) int
EventMeta func(childComplexity int) int EventID func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Values func(childComplexity int) int Meta func(childComplexity int) int
Values func(childComplexity int) int
} }
Meta struct { Meta struct {
@ -123,7 +124,7 @@ type QueryResolver interface {
SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error) SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error)
} }
type SubscriptionResolver interface { type SubscriptionResolver interface {
EventAdded(ctx context.Context, streamID string, after int64) (<-chan *model.Event, error) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *es.GQLEvent, error)
PostAdded(ctx context.Context, streamID string, after int64) (<-chan *msgbus.PostEvent, error) PostAdded(ctx context.Context, streamID string, after int64) (<-chan *msgbus.PostEvent, error)
} }
@ -156,6 +157,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Connection.Paging(childComplexity), true return e.complexity.Connection.Paging(childComplexity), true
case "Event.bytes":
if e.complexity.Event.Bytes == nil {
break
}
return e.complexity.Event.Bytes(childComplexity), true
case "Event.eventID": case "Event.eventID":
if e.complexity.Event.EventID == nil { if e.complexity.Event.EventID == nil {
break break
@ -163,13 +171,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Event.EventID(childComplexity), true return e.complexity.Event.EventID(childComplexity), true
case "Event.eventMeta":
if e.complexity.Event.EventMeta == nil {
break
}
return e.complexity.Event.EventMeta(childComplexity), true
case "Event.id": case "Event.id":
if e.complexity.Event.ID == nil { if e.complexity.Event.ID == nil {
break break
@ -177,6 +178,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Event.ID(childComplexity), true return e.complexity.Event.ID(childComplexity), true
case "Event.meta":
if e.complexity.Event.Meta == nil {
break
}
return e.complexity.Event.Meta(childComplexity), true
case "Event.values": case "Event.values":
if e.complexity.Event.Values == nil { if e.complexity.Event.Values == nil {
break break
@ -491,12 +499,13 @@ extend type Subscription {
eventAdded(streamID: String! after: Int! = -1): Event eventAdded(streamID: String! after: Int! = -1): Event
} }
type Event implements Edge { type Event implements Edge @goModel(model: "github.com/sour-is/ev/pkg/es.GQLEvent") {
id: ID! id: ID!
eventID: String! eventID: String!
values: Map! values: Map!
eventMeta: Meta! bytes: String!
meta: Meta!
}`, BuiltIn: false}, }`, BuiltIn: false},
{Name: "../../../pkg/gql/common.graphqls", Input: `scalar Time {Name: "../../../pkg/gql/common.graphqls", Input: `scalar Time
scalar Map scalar Map
@ -877,7 +886,7 @@ func (ec *executionContext) fieldContext_Connection_edges(ctx context.Context, f
return fc, nil return fc, nil
} }
func (ec *executionContext) _Event_id(ctx context.Context, field graphql.CollectedField, obj *model.Event) (ret graphql.Marshaler) { func (ec *executionContext) _Event_id(ctx context.Context, field graphql.CollectedField, obj *es.GQLEvent) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Event_id(ctx, field) fc, err := ec.fieldContext_Event_id(ctx, field)
if err != nil { if err != nil {
return graphql.Null return graphql.Null
@ -891,7 +900,7 @@ func (ec *executionContext) _Event_id(ctx context.Context, field graphql.Collect
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return obj.ID, nil return obj.ID(), nil
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -912,7 +921,7 @@ func (ec *executionContext) fieldContext_Event_id(ctx context.Context, field gra
fc = &graphql.FieldContext{ fc = &graphql.FieldContext{
Object: "Event", Object: "Event",
Field: field, Field: field,
IsMethod: false, IsMethod: true,
IsResolver: false, IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type ID does not have child fields") return nil, errors.New("field of type ID does not have child fields")
@ -921,7 +930,7 @@ func (ec *executionContext) fieldContext_Event_id(ctx context.Context, field gra
return fc, nil return fc, nil
} }
func (ec *executionContext) _Event_eventID(ctx context.Context, field graphql.CollectedField, obj *model.Event) (ret graphql.Marshaler) { func (ec *executionContext) _Event_eventID(ctx context.Context, field graphql.CollectedField, obj *es.GQLEvent) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Event_eventID(ctx, field) fc, err := ec.fieldContext_Event_eventID(ctx, field)
if err != nil { if err != nil {
return graphql.Null return graphql.Null
@ -935,7 +944,7 @@ func (ec *executionContext) _Event_eventID(ctx context.Context, field graphql.Co
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return obj.EventID, nil return obj.EventID(), nil
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -956,7 +965,7 @@ func (ec *executionContext) fieldContext_Event_eventID(ctx context.Context, fiel
fc = &graphql.FieldContext{ fc = &graphql.FieldContext{
Object: "Event", Object: "Event",
Field: field, Field: field,
IsMethod: false, IsMethod: true,
IsResolver: false, IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields") return nil, errors.New("field of type String does not have child fields")
@ -965,7 +974,7 @@ func (ec *executionContext) fieldContext_Event_eventID(ctx context.Context, fiel
return fc, nil return fc, nil
} }
func (ec *executionContext) _Event_values(ctx context.Context, field graphql.CollectedField, obj *model.Event) (ret graphql.Marshaler) { func (ec *executionContext) _Event_values(ctx context.Context, field graphql.CollectedField, obj *es.GQLEvent) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Event_values(ctx, field) fc, err := ec.fieldContext_Event_values(ctx, field)
if err != nil { if err != nil {
return graphql.Null return graphql.Null
@ -979,7 +988,7 @@ func (ec *executionContext) _Event_values(ctx context.Context, field graphql.Col
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return obj.Values, nil return obj.Values(), nil
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -1000,7 +1009,7 @@ func (ec *executionContext) fieldContext_Event_values(ctx context.Context, field
fc = &graphql.FieldContext{ fc = &graphql.FieldContext{
Object: "Event", Object: "Event",
Field: field, Field: field,
IsMethod: false, IsMethod: true,
IsResolver: false, IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Map does not have child fields") return nil, errors.New("field of type Map does not have child fields")
@ -1009,8 +1018,8 @@ func (ec *executionContext) fieldContext_Event_values(ctx context.Context, field
return fc, nil return fc, nil
} }
func (ec *executionContext) _Event_eventMeta(ctx context.Context, field graphql.CollectedField, obj *model.Event) (ret graphql.Marshaler) { func (ec *executionContext) _Event_bytes(ctx context.Context, field graphql.CollectedField, obj *es.GQLEvent) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Event_eventMeta(ctx, field) fc, err := ec.fieldContext_Event_bytes(ctx, field)
if err != nil { if err != nil {
return graphql.Null return graphql.Null
} }
@ -1023,7 +1032,51 @@ func (ec *executionContext) _Event_eventMeta(ctx context.Context, field graphql.
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return obj.EventMeta, nil return obj.Bytes()
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Event_bytes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Event",
Field: field,
IsMethod: true,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Event_meta(ctx context.Context, field graphql.CollectedField, obj *es.GQLEvent) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Event_meta(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Meta(), nil
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -1040,11 +1093,11 @@ func (ec *executionContext) _Event_eventMeta(ctx context.Context, field graphql.
return ec.marshalNMeta2ᚖgithubᚗcomᚋsourᚑisᚋevᚋpkgᚋesᚋeventᚐMeta(ctx, field.Selections, res) return ec.marshalNMeta2ᚖgithubᚗcomᚋsourᚑisᚋevᚋpkgᚋesᚋeventᚐMeta(ctx, field.Selections, res)
} }
func (ec *executionContext) fieldContext_Event_eventMeta(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { func (ec *executionContext) fieldContext_Event_meta(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{ fc = &graphql.FieldContext{
Object: "Event", Object: "Event",
Field: field, Field: field,
IsMethod: false, IsMethod: true,
IsResolver: false, IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name { switch field.Name {
@ -2269,7 +2322,7 @@ func (ec *executionContext) _Subscription_eventAdded(ctx context.Context, field
} }
return func(ctx context.Context) graphql.Marshaler { return func(ctx context.Context) graphql.Marshaler {
select { select {
case res, ok := <-resTmp.(<-chan *model.Event): case res, ok := <-resTmp.(<-chan *es.GQLEvent):
if !ok { if !ok {
return nil return nil
} }
@ -2277,7 +2330,7 @@ func (ec *executionContext) _Subscription_eventAdded(ctx context.Context, field
w.Write([]byte{'{'}) w.Write([]byte{'{'})
graphql.MarshalString(field.Alias).MarshalGQL(w) graphql.MarshalString(field.Alias).MarshalGQL(w)
w.Write([]byte{':'}) w.Write([]byte{':'})
ec.marshalOEvent2ᚖgithubᚗcomᚋsourᚑisᚋevᚋinternalᚋgraphᚋmodelᚐEvent(ctx, field.Selections, res).MarshalGQL(w) ec.marshalOEvent2ᚖgithubᚗcomᚋsourᚑisᚋevᚋpkgᚋesᚐGQLEvent(ctx, field.Selections, res).MarshalGQL(w)
w.Write([]byte{'}'}) w.Write([]byte{'}'})
}) })
case <-ctx.Done(): case <-ctx.Done():
@ -2300,8 +2353,10 @@ func (ec *executionContext) fieldContext_Subscription_eventAdded(ctx context.Con
return ec.fieldContext_Event_eventID(ctx, field) return ec.fieldContext_Event_eventID(ctx, field)
case "values": case "values":
return ec.fieldContext_Event_values(ctx, field) return ec.fieldContext_Event_values(ctx, field)
case "eventMeta": case "bytes":
return ec.fieldContext_Event_eventMeta(ctx, field) return ec.fieldContext_Event_bytes(ctx, field)
case "meta":
return ec.fieldContext_Event_meta(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Event", field.Name) return nil, fmt.Errorf("no field named %q was found under type Event", field.Name)
}, },
@ -4263,9 +4318,7 @@ func (ec *executionContext) _Edge(ctx context.Context, sel ast.SelectionSet, obj
switch obj := (obj).(type) { switch obj := (obj).(type) {
case nil: case nil:
return graphql.Null return graphql.Null
case model.Event: case *es.GQLEvent:
return ec._Event(ctx, sel, &obj)
case *model.Event:
if obj == nil { if obj == nil {
return graphql.Null return graphql.Null
} }
@ -4321,7 +4374,7 @@ func (ec *executionContext) _Connection(ctx context.Context, sel ast.SelectionSe
var eventImplementors = []string{"Event", "Edge"} var eventImplementors = []string{"Event", "Edge"}
func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, obj *model.Event) graphql.Marshaler { func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, obj *es.GQLEvent) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, eventImplementors) fields := graphql.CollectFields(ec.OperationContext, sel, eventImplementors)
out := graphql.NewFieldSet(fields) out := graphql.NewFieldSet(fields)
var invalids uint32 var invalids uint32
@ -4350,9 +4403,16 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "eventMeta": case "bytes":
out.Values[i] = ec._Event_eventMeta(ctx, field, obj) out.Values[i] = ec._Event_bytes(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "meta":
out.Values[i] = ec._Event_meta(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
@ -5658,7 +5718,7 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
return res return res
} }
func (ec *executionContext) marshalOEvent2ᚖgithubᚗcomᚋsourᚑisᚋevᚋinternalᚋgraphᚋmodelᚐEvent(ctx context.Context, sel ast.SelectionSet, v *model.Event) graphql.Marshaler { func (ec *executionContext) marshalOEvent2ᚖgithubᚗcomᚋsourᚑisᚋevᚋpkgᚋesᚐGQLEvent(ctx context.Context, sel ast.SelectionSet, v *es.GQLEvent) graphql.Marshaler {
if v == nil { if v == nil {
return graphql.Null return graphql.Null
} }

View File

@ -1,16 +1,3 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. // Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model package model
import (
"github.com/sour-is/ev/pkg/es/event"
)
type Event struct {
ID string `json:"id"`
EventID string `json:"eventID"`
Values map[string]interface{} `json:"values"`
EventMeta *event.Meta `json:"eventMeta"`
}
func (Event) IsEdge() {}

28
main.go
View File

@ -68,6 +68,8 @@ func run(ctx context.Context) error {
enable := set(strings.Fields(env("EV_ENABLE", "salty msgbus gql peers"))...) enable := set(strings.Fields(env("EV_ENABLE", "salty msgbus gql peers"))...)
var svcs []interface{ RegisterHTTP(*http.ServeMux) } var svcs []interface{ RegisterHTTP(*http.ServeMux) }
svcs = append(svcs, es)
if enable.Has("salty") { if enable.Has("salty") {
span.AddEvent("Enable Salty") span.AddEvent("Enable Salty")
salty, err := salty.New(ctx, es, path.Join(env("EV_BASE_URL", "http://"+s.Addr), "inbox")) salty, err := salty.New(ctx, es, path.Join(env("EV_BASE_URL", "http://"+s.Addr), "inbox"))
@ -148,10 +150,15 @@ func env(name, defaultValue string) string {
return defaultValue return defaultValue
} }
func httpMux(fns ...interface{ RegisterHTTP(*http.ServeMux) }) http.Handler { func httpMux(fns ...interface{ RegisterHTTP(*http.ServeMux) }) http.Handler {
mux := http.NewServeMux() mux := newMux()
for _, fn := range fns { for _, fn := range fns {
fn.RegisterHTTP(mux) fn.RegisterHTTP(mux.ServeMux)
if fn, ok := fn.(interface{ RegisterAPIv1(*http.ServeMux) }); ok {
fn.RegisterAPIv1(mux.api)
}
} }
return cors.AllowAll().Handler(mux) return cors.AllowAll().Handler(mux)
} }
@ -183,3 +190,20 @@ func (s Set[T]) String() string {
b.WriteString(")") b.WriteString(")")
return b.String() return b.String()
} }
type mux struct {
*http.ServeMux
api *http.ServeMux
}
func newMux() *mux {
mux := &mux{
api: http.NewServeMux(),
ServeMux: http.NewServeMux(),
}
mux.Handle("/api/v1/", http.StripPrefix("/api/v1/", mux.api))
return mux
}
func (m mux) HandleAPIv1(pattern string, handler http.Handler) {
m.api.Handle(pattern, handler)
}

View File

@ -14,10 +14,11 @@ extend type Subscription {
eventAdded(streamID: String! after: Int! = -1): Event eventAdded(streamID: String! after: Int! = -1): Event
} }
type Event implements Edge { type Event implements Edge @goModel(model: "github.com/sour-is/ev/pkg/es.GQLEvent") {
id: ID! id: ID!
eventID: String! eventID: String!
values: Map! values: Map!
eventMeta: Meta! bytes: String!
meta: Meta!
} }

View File

@ -247,11 +247,22 @@ func embedJSON(s string) json.RawMessage {
return []byte(fmt.Sprintf(`"%s"`, strings.Replace(s, `"`, `\"`, -1))) return []byte(fmt.Sprintf(`"%s"`, strings.Replace(s, `"`, `\"`, -1)))
} }
func values(e Event) map[string]any { func Values(e Event) map[string]any {
m := make(map[string]any) var a any = e
v := reflect.ValueOf(e)
for _, idx := range reflect.VisibleFields(v) { if e, ok := e.(interface{ Values() any }); ok {
field := v.FieldByIndex(idx.Index) a = e.Values()
m[field.]
} }
m := make(map[string]any)
v := reflect.Indirect(reflect.ValueOf(a))
for _, idx := range reflect.VisibleFields(v.Type()) {
if !idx.IsExported() {
continue
}
field := v.FieldByIndex(idx.Index)
m[idx.Name] = field.Interface()
}
return m
} }

128
pkg/es/graph.go Normal file
View File

@ -0,0 +1,128 @@
package es
import (
"context"
"fmt"
"net/http"
"time"
"github.com/sour-is/ev/internal/logz"
"github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/gql"
)
type EventResolver interface {
Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error)
EventAdded(ctx context.Context, streamID string, after int64) (<-chan *GQLEvent, error)
}
func (es *EventStore) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
ctx, span := logz.Span(ctx)
defer span.End()
lis, err := es.Read(ctx, streamID, paging.GetIdx(0), paging.GetCount(30))
if err != nil {
span.RecordError(err)
return nil, err
}
edges := make([]gql.Edge, 0, len(lis))
for i := range lis {
span.AddEvent(fmt.Sprint("event ", i, " of ", len(lis)))
edges = append(edges, &GQLEvent{lis[i]})
}
var first, last uint64
if first, err = es.FirstIndex(ctx, streamID); err != nil {
span.RecordError(err)
return nil, err
}
if last, err = es.LastIndex(ctx, streamID); err != nil {
span.RecordError(err)
return nil, err
}
return &gql.Connection{
Paging: &gql.PageInfo{
Next: lis.Last().EventMeta().Position < last,
Prev: lis.First().EventMeta().Position > first,
Begin: lis.First().EventMeta().Position,
End: lis.Last().EventMeta().Position,
},
Edges: edges,
}, nil
}
func (e *EventStore) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *GQLEvent, error) {
ctx, span := logz.Span(ctx)
defer span.End()
es := e.EventStream()
if es == nil {
return nil, fmt.Errorf("EventStore does not implement streaming")
}
sub, err := es.Subscribe(ctx, streamID, after)
if err != nil {
span.RecordError(err)
return nil, err
}
ch := make(chan *GQLEvent)
go func() {
ctx, span := logz.Span(ctx)
defer span.End()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
err := sub.Close(ctx)
span.RecordError(err)
}()
for sub.Recv(ctx) {
events, err := sub.Events(ctx)
if err != nil {
span.RecordError(err)
break
}
span.AddEvent(fmt.Sprintf("received %d events", len(events)))
for i := range events {
select {
case ch <- &GQLEvent{events[i]}:
continue
case <-ctx.Done():
return
}
}
}
}()
return ch, nil
}
func (*EventStore) RegisterHTTP(*http.ServeMux) {}
type GQLEvent struct {
e event.Event
}
func (e *GQLEvent) ID() string {
return "Event/" + e.e.EventMeta().GetEventID()
}
func (e *GQLEvent) EventID() string {
return e.e.EventMeta().GetEventID()
}
func (e *GQLEvent) Values() map[string]interface{} {
return event.Values(e.e)
}
func (e *GQLEvent) Bytes() (string, error) {
b, err := e.e.MarshalBinary()
return string(b), err
}
func (e *GQLEvent) Meta() *event.Meta {
meta := e.e.EventMeta()
return &meta
}
func (e *GQLEvent) IsEdge() {}