feat: add graphql
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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
139
pkg/es/service/service.go
Normal 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, "/")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
62
pkg/playground/playground.go
Normal file
62
pkg/playground/playground.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user