feat: updates
This commit is contained in:
parent
8c54eefcdd
commit
f65e9ca666
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/sour-is/ev/app/salty"
|
||||
"github.com/sour-is/ev/internal/graph/generated"
|
||||
"github.com/sour-is/ev/internal/logz"
|
||||
"github.com/sour-is/ev/pkg/es"
|
||||
"github.com/sour-is/ev/pkg/gql"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
)
|
||||
|
@ -22,6 +23,7 @@ import (
|
|||
type Resolver struct {
|
||||
msgbus.MsgbusResolver
|
||||
salty.SaltyResolver
|
||||
es.EventResolver
|
||||
}
|
||||
|
||||
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()) {
|
||||
span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
|
||||
field.Set(rs)
|
||||
break outer
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
|
||||
|
||||
field.Set(noop)
|
||||
|
||||
}
|
||||
|
||||
return r, nil
|
||||
|
@ -107,6 +108,7 @@ func NoopRecover(ctx context.Context, err interface{}) error {
|
|||
|
||||
var _ msgbus.MsgbusResolver = (*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) {
|
||||
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) {
|
||||
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) {}
|
||||
|
|
|
@ -435,14 +435,21 @@ func (e *PostEvent) SetEventMeta(eventMeta event.Meta) {
|
|||
}
|
||||
e.eventMeta = eventMeta
|
||||
}
|
||||
func (e *PostEvent) MarshalBinary() ([]byte, error) {
|
||||
j := struct {
|
||||
func (e *PostEvent) Values() any {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return struct {
|
||||
Payload []byte
|
||||
Tags []string
|
||||
}{
|
||||
Payload: e.payload,
|
||||
Tags: e.tags,
|
||||
}
|
||||
}
|
||||
func (e *PostEvent) MarshalBinary() ([]byte, error) {
|
||||
j := e.Values()
|
||||
return json.Marshal(&j)
|
||||
}
|
||||
func (e *PostEvent) UnmarshalBinary(b []byte) error {
|
||||
|
|
142
app/salty/salty-addr.go
Normal file
142
app/salty/salty-addr.go
Normal 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
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
@ -19,12 +20,21 @@ import (
|
|||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
type DNSResolver interface {
|
||||
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
baseURL string
|
||||
es *es.EventStore
|
||||
dns DNSResolver
|
||||
|
||||
Mresolver_create_salty_user syncint64.Counter
|
||||
Mresolver_salty_user syncint64.Counter
|
||||
m_create_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 {
|
||||
name string
|
||||
|
@ -51,13 +61,25 @@ func New(ctx context.Context, es *es.EventStore, baseURL string) (*service, erro
|
|||
|
||||
m := logz.Meter(ctx)
|
||||
|
||||
svc := &service{baseURL: baseURL, es: es}
|
||||
svc := &service{baseURL: baseURL, es: es, dns: net.DefaultResolver}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
span.RecordError(err)
|
||||
|
||||
|
@ -73,6 +95,12 @@ func (s *service) BaseURL() string {
|
|||
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||
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) {
|
||||
ctx := r.Context()
|
||||
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)
|
||||
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))))
|
||||
span.AddEvent(streamID)
|
||||
|
@ -142,7 +170,7 @@ func (s *service) SaltyUser(ctx context.Context, nick string) (*SaltyUser, error
|
|||
ctx, span := logz.Span(ctx)
|
||||
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))))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import (
|
|||
"github.com/99designs/gqlgen/plugin/federation/fedruntime"
|
||||
"github.com/sour-is/ev/app/msgbus"
|
||||
"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/gql"
|
||||
gqlparser "github.com/vektah/gqlparser/v2"
|
||||
|
@ -58,9 +58,10 @@ type ComplexityRoot struct {
|
|||
}
|
||||
|
||||
Event struct {
|
||||
Bytes func(childComplexity int) int
|
||||
EventID func(childComplexity int) int
|
||||
EventMeta func(childComplexity int) int
|
||||
ID func(childComplexity int) int
|
||||
Meta func(childComplexity int) int
|
||||
Values func(childComplexity int) int
|
||||
}
|
||||
|
||||
|
@ -123,7 +124,7 @@ type QueryResolver interface {
|
|||
SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -156,6 +157,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||
|
||||
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":
|
||||
if e.complexity.Event.EventID == nil {
|
||||
break
|
||||
|
@ -163,13 +171,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||
|
||||
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":
|
||||
if e.complexity.Event.ID == nil {
|
||||
break
|
||||
|
@ -177,6 +178,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||
|
||||
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":
|
||||
if e.complexity.Event.Values == nil {
|
||||
break
|
||||
|
@ -491,12 +499,13 @@ extend type Subscription {
|
|||
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!
|
||||
|
||||
eventID: String!
|
||||
values: Map!
|
||||
eventMeta: Meta!
|
||||
bytes: String!
|
||||
meta: Meta!
|
||||
}`, BuiltIn: false},
|
||||
{Name: "../../../pkg/gql/common.graphqls", Input: `scalar Time
|
||||
scalar Map
|
||||
|
@ -877,7 +886,7 @@ func (ec *executionContext) fieldContext_Connection_edges(ctx context.Context, f
|
|||
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)
|
||||
if err != nil {
|
||||
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) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.ID, nil
|
||||
return obj.ID(), nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
|
@ -912,7 +921,7 @@ func (ec *executionContext) fieldContext_Event_id(ctx context.Context, field gra
|
|||
fc = &graphql.FieldContext{
|
||||
Object: "Event",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsMethod: true,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.EventID, nil
|
||||
return obj.EventID(), nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
|
@ -956,7 +965,7 @@ func (ec *executionContext) fieldContext_Event_eventID(ctx context.Context, fiel
|
|||
fc = &graphql.FieldContext{
|
||||
Object: "Event",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
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")
|
||||
|
@ -965,7 +974,7 @@ func (ec *executionContext) fieldContext_Event_eventID(ctx context.Context, fiel
|
|||
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)
|
||||
if err != nil {
|
||||
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) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.Values, nil
|
||||
return obj.Values(), nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
|
@ -1000,7 +1009,7 @@ func (ec *executionContext) fieldContext_Event_values(ctx context.Context, field
|
|||
fc = &graphql.FieldContext{
|
||||
Object: "Event",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsMethod: true,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
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
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Event_eventMeta(ctx context.Context, field graphql.CollectedField, obj *model.Event) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_Event_eventMeta(ctx, field)
|
||||
func (ec *executionContext) _Event_bytes(ctx context.Context, field graphql.CollectedField, obj *es.GQLEvent) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_Event_bytes(ctx, field)
|
||||
if err != nil {
|
||||
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) {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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{
|
||||
Object: "Event",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsMethod: true,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
switch field.Name {
|
||||
|
@ -2269,7 +2322,7 @@ func (ec *executionContext) _Subscription_eventAdded(ctx context.Context, field
|
|||
}
|
||||
return func(ctx context.Context) graphql.Marshaler {
|
||||
select {
|
||||
case res, ok := <-resTmp.(<-chan *model.Event):
|
||||
case res, ok := <-resTmp.(<-chan *es.GQLEvent):
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
@ -2277,7 +2330,7 @@ func (ec *executionContext) _Subscription_eventAdded(ctx context.Context, field
|
|||
w.Write([]byte{'{'})
|
||||
graphql.MarshalString(field.Alias).MarshalGQL(w)
|
||||
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{'}'})
|
||||
})
|
||||
case <-ctx.Done():
|
||||
|
@ -2300,8 +2353,10 @@ func (ec *executionContext) fieldContext_Subscription_eventAdded(ctx context.Con
|
|||
return ec.fieldContext_Event_eventID(ctx, field)
|
||||
case "values":
|
||||
return ec.fieldContext_Event_values(ctx, field)
|
||||
case "eventMeta":
|
||||
return ec.fieldContext_Event_eventMeta(ctx, field)
|
||||
case "bytes":
|
||||
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)
|
||||
},
|
||||
|
@ -4263,9 +4318,7 @@ func (ec *executionContext) _Edge(ctx context.Context, sel ast.SelectionSet, obj
|
|||
switch obj := (obj).(type) {
|
||||
case nil:
|
||||
return graphql.Null
|
||||
case model.Event:
|
||||
return ec._Event(ctx, sel, &obj)
|
||||
case *model.Event:
|
||||
case *es.GQLEvent:
|
||||
if obj == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
|
@ -4321,7 +4374,7 @@ func (ec *executionContext) _Connection(ctx context.Context, sel ast.SelectionSe
|
|||
|
||||
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)
|
||||
out := graphql.NewFieldSet(fields)
|
||||
var invalids uint32
|
||||
|
@ -4350,9 +4403,16 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob
|
|||
if out.Values[i] == graphql.Null {
|
||||
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 {
|
||||
invalids++
|
||||
|
@ -5658,7 +5718,7 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
|
|||
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 {
|
||||
return graphql.Null
|
||||
}
|
||||
|
|
|
@ -1,16 +1,3 @@
|
|||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
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
28
main.go
|
@ -68,6 +68,8 @@ func run(ctx context.Context) error {
|
|||
enable := set(strings.Fields(env("EV_ENABLE", "salty msgbus gql peers"))...)
|
||||
var svcs []interface{ RegisterHTTP(*http.ServeMux) }
|
||||
|
||||
svcs = append(svcs, es)
|
||||
|
||||
if enable.Has("salty") {
|
||||
span.AddEvent("Enable Salty")
|
||||
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
|
||||
}
|
||||
func httpMux(fns ...interface{ RegisterHTTP(*http.ServeMux) }) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux := newMux()
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -183,3 +190,20 @@ func (s Set[T]) String() string {
|
|||
b.WriteString(")")
|
||||
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)
|
||||
}
|
|
@ -14,10 +14,11 @@ extend type Subscription {
|
|||
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!
|
||||
|
||||
eventID: String!
|
||||
values: Map!
|
||||
eventMeta: Meta!
|
||||
bytes: String!
|
||||
meta: Meta!
|
||||
}
|
|
@ -247,11 +247,22 @@ func embedJSON(s string) json.RawMessage {
|
|||
return []byte(fmt.Sprintf(`"%s"`, strings.Replace(s, `"`, `\"`, -1)))
|
||||
}
|
||||
|
||||
func values(e Event) map[string]any {
|
||||
m := make(map[string]any)
|
||||
v := reflect.ValueOf(e)
|
||||
for _, idx := range reflect.VisibleFields(v) {
|
||||
field := v.FieldByIndex(idx.Index)
|
||||
m[field.]
|
||||
func Values(e Event) map[string]any {
|
||||
var a any = e
|
||||
|
||||
if e, ok := e.(interface{ Values() any }); ok {
|
||||
a = e.Values()
|
||||
}
|
||||
|
||||
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
128
pkg/es/graph.go
Normal 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() {}
|
Loading…
Reference in New Issue
Block a user