feat: add graphql

This commit is contained in:
Jon Lundy
2022-08-07 11:55:49 -06:00
parent f436393965
commit 82f23ae323
20 changed files with 5234 additions and 170 deletions

View File

@@ -53,11 +53,9 @@ func (es *diskStore) Save(ctx context.Context, agg event.Aggregate) (uint64, err
}
var last uint64
if last, err = l.w.LastIndex(); err != nil {
if last, err = l.LastIndex(); err != nil {
return 0, err
}
if agg.StreamVersion() != last {
return 0, fmt.Errorf("current version wrong %d != %d", agg.StreamVersion(), last)
}
@@ -75,7 +73,7 @@ func (es *diskStore) Save(ctx context.Context, agg event.Aggregate) (uint64, err
batch.Write(e.EventMeta().Position, b)
}
err = l.w.WriteBatch(batch)
err = l.WriteBatch(batch)
if err != nil {
return 0, err
}
@@ -92,8 +90,7 @@ func (es *diskStore) Append(ctx context.Context, streamID string, events event.E
}
var last uint64
if last, err = l.w.LastIndex(); err != nil {
if last, err = l.LastIndex(); err != nil {
return 0, err
}
@@ -111,7 +108,7 @@ func (es *diskStore) Append(ctx context.Context, streamID string, events event.E
batch.Write(pos, b)
}
err = l.w.WriteBatch(batch)
err = l.WriteBatch(batch)
if err != nil {
return 0, err
}
@@ -125,22 +122,20 @@ func (es *diskStore) Load(ctx context.Context, agg event.Aggregate) error {
var i, first, last uint64
if first, err = l.w.FirstIndex(); err != nil {
if first, err = l.FirstIndex(); err != nil {
return err
}
if last, err = l.w.LastIndex(); err != nil {
if last, err = l.LastIndex(); err != nil {
return err
}
if first == 0 || last == 0 {
return nil
}
var b []byte
events := make([]event.Event, last-i)
for i = 0; first+i <= last; i++ {
b, err = l.w.Read(first + i)
b, err = l.Read(first + i)
if err != nil {
return err
}
@@ -149,7 +144,6 @@ func (es *diskStore) Load(ctx context.Context, agg event.Aggregate) error {
return err
}
}
event.Append(agg, events...)
return nil
@@ -161,14 +155,12 @@ func (es *diskStore) Read(ctx context.Context, streamID string, pos, count int64
}
var first, last, start uint64
if first, err = l.w.FirstIndex(); err != nil {
if first, err = l.FirstIndex(); err != nil {
return nil, err
}
if last, err = l.w.LastIndex(); err != nil {
if last, err = l.LastIndex(); err != nil {
return nil, err
}
if first == 0 || last == 0 {
return nil, nil
}
@@ -190,7 +182,7 @@ func (es *diskStore) Read(ctx context.Context, streamID string, pos, count int64
for i := range events {
var b []byte
b, err = l.w.Read(start)
b, err = l.Read(start)
if err != nil {
return events, err
}
@@ -209,25 +201,25 @@ func (es *diskStore) Read(ctx context.Context, streamID string, pos, count int64
break
}
}
event.SetStreamID(streamID, events...)
return events, nil
}
func (es *diskStore) readLog(name string) (*eventLog, error) {
return newEventLog(name, filepath.Join(es.path, name))
func (es *diskStore) FirstIndex(ctx context.Context, streamID string) (uint64, error) {
l, err := es.readLog(streamID)
if err != nil {
return 0, err
}
return l.FirstIndex()
}
func (es *diskStore) LastIndex(ctx context.Context, streamID string) (uint64, error) {
l, err := es.readLog(streamID)
if err != nil {
return 0, err
}
return l.LastIndex()
}
type eventLog struct {
name string
path string
w *wal.Log
}
func newEventLog(name, path string) (*eventLog, error) {
var err error
el := &eventLog{name: name, path: path}
el.w, err = wal.Open(path, wal.DefaultOptions)
return el, err
func (es *diskStore) readLog(name string) (*wal.Log, error) {
return wal.Open(filepath.Join(es.path, name), wal.DefaultOptions)
}

View File

@@ -15,4 +15,6 @@ type EventStore interface {
Load(ctx context.Context, agg event.Aggregate) error
Read(ctx context.Context, streamID string, pos, count int64) (event.Events, error)
Append(ctx context.Context, streamID string, events event.Events) (uint64, error)
FirstIndex(ctx context.Context, streamID string) (uint64, error)
LastIndex(ctx context.Context, streamID string) (uint64, error)
}

View File

@@ -140,3 +140,19 @@ func (m *memstore) Save(ctx context.Context, agg event.Aggregate) (uint64, error
return uint64(len(events)), nil
}
func (m *memstore) FirstIndex(ctx context.Context, streamID string) (uint64, error) {
stream, err := m.state.Copy(ctx)
if err != nil {
return 0, err
}
return stream.streams[streamID].First().EventMeta().Position, nil
}
func (m *memstore) LastIndex(ctx context.Context, streamID string) (uint64, error) {
stream, err := m.state.Copy(ctx)
if err != nil {
return 0, err
}
return stream.streams[streamID].Last().EventMeta().Position, nil
}

View File

@@ -131,9 +131,10 @@ type Meta struct {
Position uint64
}
func (m Meta) Time() time.Time {
func (m Meta) Created() time.Time {
return ulid.Time(m.EventID.Time())
}
func (m Meta) ID() string { return m.EventID.String() }
type _nilEvent struct{}

139
pkg/es/service/service.go Normal file
View File

@@ -0,0 +1,139 @@
package service
import (
"bytes"
"context"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event"
)
type service struct {
es driver.EventStore
}
func New(ctx context.Context, es driver.EventStore) (*service, error) {
if err := event.Register(ctx, &PostEvent{}); err != nil {
return nil, err
}
return &service{es}, nil
}
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
name, tags, _ := strings.Cut(r.URL.Path, "/")
if name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
switch r.Method {
case http.MethodGet:
var pos, count int64 = -1, -99
qry := r.URL.Query()
if i, err := strconv.ParseInt(qry.Get("idx"), 10, 64); err == nil {
pos = i
}
if i, err := strconv.ParseInt(qry.Get("n"), 10, 64); err == nil {
count = i
}
log.Print("GET topic=", name, " idx=", pos, " n=", count)
events, err := s.es.Read(ctx, "post-"+name, pos, count)
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
for i := range events {
fmt.Fprintln(w, events[i])
}
return
case http.MethodPost, http.MethodPut:
b, err := io.ReadAll(io.LimitReader(r.Body, 64*1024))
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusBadRequest)
return
}
r.Body.Close()
if name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
events := event.NewEvents(&PostEvent{
Payload: b,
Tags: fields(tags),
})
_, err = s.es.Append(r.Context(), "post-"+name, events)
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
m := events.First().EventMeta()
w.WriteHeader(http.StatusAccepted)
log.Print("POST topic=", name, " tags=", tags, " idx=", m.Position, " id=", m.EventID)
fmt.Fprintf(w, "OK %d %s", m.Position, m.EventID)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
type PostEvent struct {
Payload []byte
Tags []string
eventMeta event.Meta
}
func (e *PostEvent) EventMeta() event.Meta {
if e == nil {
return event.Meta{}
}
return e.eventMeta
}
func (e *PostEvent) SetEventMeta(eventMeta event.Meta) {
if e == nil {
return
}
e.eventMeta = eventMeta
}
func (e *PostEvent) String() string {
var b bytes.Buffer
// b.WriteString(e.eventMeta.StreamID)
// b.WriteRune('@')
b.WriteString(strconv.FormatUint(e.eventMeta.Position, 10))
b.WriteRune('\t')
b.WriteString(e.eventMeta.EventID.String())
b.WriteRune('\t')
b.WriteString(string(e.Payload))
if len(e.Tags) > 0 {
b.WriteRune('\t')
b.WriteString(strings.Join(e.Tags, ","))
}
return b.String()
}
func fields(s string) []string {
if s == "" {
return nil
}
return strings.Split(s, "/")
}

View File

@@ -6,6 +6,7 @@ type Locked[T any] struct {
state chan *T
}
// New creates a new locker for the given value.
func New[T any](initial *T) *Locked[T] {
s := &Locked[T]{}
s.state = make(chan *T, 1)
@@ -13,6 +14,7 @@ func New[T any](initial *T) *Locked[T] {
return s
}
// Modify will call the function with the locked value
func (s *Locked[T]) Modify(ctx context.Context, fn func(*T) error) error {
if ctx.Err() != nil {
return ctx.Err()
@@ -27,6 +29,7 @@ func (s *Locked[T]) Modify(ctx context.Context, fn func(*T) error) error {
}
}
// Copy will return a shallow copy of the locked object.
func (s *Locked[T]) Copy(ctx context.Context) (T, error) {
var t T

View File

@@ -0,0 +1,62 @@
package playground
import (
"html/template"
"net/http"
)
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8/>
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
<link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"
integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"
integrity="{{ .faviconSRI }}" crossorigin="anonymous"/>
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"
integrity="{{ .jsSRI }}" crossorigin="anonymous"></script>
<title>{{.title}}</title>
</head>
<body>
<style type="text/css">
html { font-family: "Open Sans", sans-serif; overflow: hidden; }
body { margin: 0; background: #172a3a; }
</style>
<div id="root"/>
<script type="text/javascript">
window.addEventListener('load', function (event) {
const root = document.getElementById('root');
root.classList.add('playgroundIn');
const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:'
GraphQLPlayground.init(root, {
endpoint: location.protocol + '//' + location.host + '{{.endpoint}}',
subscriptionsEndpoint: wsProto + '//' + location.host + '{{.endpoint }}',
shareEnabled: true,
settings: {
'request.credentials': 'same-origin'
}
})
})
</script>
</body>
</html>
`))
func Handler(title string, endpoint string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
err := page.Execute(w, map[string]string{
"title": title,
"endpoint": endpoint,
"version": "1.7.26",
"cssSRI": "sha256-dKnNLEFwKSVFpkpjRWe+o/jQDM6n/JsvQ0J3l5Dk3fc=",
"faviconSRI": "sha256-GhTyE+McTU79R4+pRO6ih+4TfsTOrpPwD8ReKFzb3PM=",
"jsSRI": "sha256-SG9YAy4eywTcLckwij7V4oSCG3hOdV1m+2e1XuNxIgk=",
})
if err != nil {
panic(err)
}
}
}