feat: add graphql
This commit is contained in:
parent
f436393965
commit
82f23ae323
13
Makefile
13
Makefile
|
@ -4,4 +4,15 @@ export EV_DATA = mem:
|
|||
run:
|
||||
go run .
|
||||
test:
|
||||
go test -cover -race ./...
|
||||
go test -cover -race ./...
|
||||
|
||||
|
||||
GQLDIR=api/gql_ev
|
||||
GQLS=$(wildcard $(GQLDIR)/*.go) $(wildcard $(GQLDIR)/*.graphqls) gqlgen.yml
|
||||
GQLSRC=internal/ev/graph/generated/generated.go
|
||||
|
||||
gen: gql
|
||||
gql: $(GQLSRC)
|
||||
$(GQLSRC): $(GQLS)
|
||||
go get github.com/99designs/gqlgen@latest
|
||||
go run github.com/99designs/gqlgen
|
1
api/gql_ev/docs.go
Normal file
1
api/gql_ev/docs.go
Normal file
|
@ -0,0 +1 @@
|
|||
package gql_ev
|
46
api/gql_ev/models.go
Normal file
46
api/gql_ev/models.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package gql_ev
|
||||
|
||||
import "github.com/sour-is/ev/pkg/es/event"
|
||||
|
||||
type Edge interface {
|
||||
IsEdge()
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
Paging *PageInfo `json:"paging"`
|
||||
Edges []Edge `json:"edges"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
ID string `json:"id"`
|
||||
Payload string `json:"payload"`
|
||||
Tags []string `json:"tags"`
|
||||
Meta *event.Meta `json:"meta"`
|
||||
}
|
||||
|
||||
func (Event) IsEdge() {}
|
||||
|
||||
type PageInfo struct {
|
||||
Next bool `json:"next"`
|
||||
Prev bool `json:"prev"`
|
||||
Begin uint64 `json:"begin"`
|
||||
End uint64 `json:"end"`
|
||||
}
|
||||
|
||||
type PageInput struct {
|
||||
Idx *int64 `json:"idx"`
|
||||
Count *int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (p *PageInput) GetIdx(v int64) int64 {
|
||||
if p == nil || p.Idx == nil {
|
||||
return v
|
||||
}
|
||||
return *p.Idx
|
||||
}
|
||||
func (p *PageInput) GetCount(v int64) int64 {
|
||||
if p == nil || p.Count == nil {
|
||||
return v
|
||||
}
|
||||
return *p.Count
|
||||
}
|
64
api/gql_ev/resolver.go
Normal file
64
api/gql_ev/resolver.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package gql_ev
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sour-is/ev/pkg/es/driver"
|
||||
"github.com/sour-is/ev/pkg/es/service"
|
||||
)
|
||||
|
||||
// This file will not be regenerated automatically.
|
||||
//
|
||||
// It serves as dependency injection for your app, add any dependencies you require here.
|
||||
|
||||
type Resolver struct {
|
||||
es driver.EventStore
|
||||
}
|
||||
|
||||
func New(es driver.EventStore) *Resolver {
|
||||
return &Resolver{es}
|
||||
}
|
||||
|
||||
// Events is the resolver for the events field.
|
||||
func (r *Resolver) Events(ctx context.Context, streamID string, paging *PageInput) (*Connection, error) {
|
||||
lis, err := r.es.Read(ctx, streamID, paging.GetIdx(0), paging.GetCount(30))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edges := make([]Edge, 0, len(lis))
|
||||
for i := range lis {
|
||||
e := lis[i]
|
||||
m := e.EventMeta()
|
||||
|
||||
post, ok := e.(*service.PostEvent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
edges = append(edges, Event{
|
||||
ID: lis[i].EventMeta().EventID.String(),
|
||||
Payload: string(post.Payload),
|
||||
Tags: post.Tags,
|
||||
Meta: &m,
|
||||
})
|
||||
}
|
||||
|
||||
var first, last uint64
|
||||
if first, err = r.es.FirstIndex(ctx, streamID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if last, err = r.es.LastIndex(ctx, streamID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Connection{
|
||||
Paging: &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
|
||||
}
|
46
api/gql_ev/schema.graphqls
Normal file
46
api/gql_ev/schema.graphqls
Normal file
|
@ -0,0 +1,46 @@
|
|||
extend type Query {
|
||||
events(streamID: String! paging: PageInput): Connection!
|
||||
}
|
||||
|
||||
type Connection {
|
||||
paging: PageInfo!
|
||||
edges: [Edge!]!
|
||||
}
|
||||
input PageInput {
|
||||
idx: Int = 0
|
||||
count: Int = 30
|
||||
}
|
||||
type PageInfo {
|
||||
next: Boolean!
|
||||
prev: Boolean!
|
||||
|
||||
begin: Int!
|
||||
end: Int!
|
||||
}
|
||||
interface Edge {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Event implements Edge {
|
||||
id: ID!
|
||||
|
||||
payload: String!
|
||||
tags: [String!]!
|
||||
|
||||
meta: Meta!
|
||||
}
|
||||
|
||||
type Meta {
|
||||
id: String!
|
||||
|
||||
streamID: String!
|
||||
created: Time!
|
||||
position: Int!
|
||||
}
|
||||
|
||||
scalar Time
|
||||
|
||||
directive @goField(
|
||||
forceResolver: Boolean
|
||||
name: String
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
9
go.mod
9
go.mod
|
@ -3,10 +3,19 @@ module github.com/sour-is/ev
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.13
|
||||
github.com/tidwall/wal v1.1.7
|
||||
github.com/vektah/gqlparser/v2 v2.4.6
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.3.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/matryer/is v1.4.0
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
|
|
89
go.sum
89
go.sum
|
@ -1,8 +1,49 @@
|
|||
github.com/99designs/gqlgen v0.17.13 h1:ETUEqvRg5Zvr1lXtpoRdj026fzVay0ZlJPwI33qXLIw=
|
||||
github.com/99designs/gqlgen v0.17.13/go.mod h1:w1brbeOdqVyNJI553BGwtwdVcYu1LKeYE1opLWN9RgQ=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA=
|
||||
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
|
||||
github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
|
@ -13,5 +54,53 @@ github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I
|
|||
github.com/tidwall/tinylru v1.1.0/go.mod h1:3+bX+TJ2baOLMWTnlyNWHh4QMnFyARg2TLTQ6OFbzw8=
|
||||
github.com/tidwall/wal v1.1.7 h1:emc1TRjIVsdKKSnpwGBAcsAGg0767SvUk8+ygx7Bb+4=
|
||||
github.com/tidwall/wal v1.1.7/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E=
|
||||
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
|
||||
github.com/vektah/gqlparser/v2 v2.4.6 h1:Yjzp66g6oVq93Jihbi0qhGnf/6zIWjcm8H6gA27zstE=
|
||||
github.com/vektah/gqlparser/v2 v2.4.6/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
69
gqlgen.yml
Normal file
69
gqlgen.yml
Normal file
|
@ -0,0 +1,69 @@
|
|||
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
|
||||
schema:
|
||||
- api/gql_ev/*.graphqls
|
||||
|
||||
# Where should the generated server code go?
|
||||
exec:
|
||||
filename: internal/graph/generated/generated.go
|
||||
package: generated
|
||||
|
||||
# Uncomment to enable federation
|
||||
federation:
|
||||
filename: internal/graph/generated/federation.go
|
||||
package: generated
|
||||
|
||||
# Where should any generated models go?
|
||||
model:
|
||||
filename: internal/graph/model/models_gen.go
|
||||
package: model
|
||||
|
||||
# Where should the resolver implementations go?
|
||||
# resolver:
|
||||
# layout: follow-schema
|
||||
# dir: internal/graph
|
||||
# package: graph
|
||||
|
||||
# Optional: turn on use `gqlgen:"fieldName"` tags in your models
|
||||
# struct_tag: json
|
||||
|
||||
# Optional: turn on to use []Thing instead of []*Thing
|
||||
# omit_slice_element_pointers: false
|
||||
|
||||
# Optional: set to speed up generation time by not performing a final validation pass.
|
||||
# skip_validation: true
|
||||
|
||||
# gqlgen will search for any type names in the schema in these go packages
|
||||
# if they match it will use them, otherwise it will generate them.
|
||||
autobind:
|
||||
- "github.com/sour-is/ev/api/gql_ev"
|
||||
|
||||
# This section declares type mapping between the GraphQL and go type systems
|
||||
#
|
||||
# The first line in each type will be used as defaults for resolver arguments and
|
||||
# modelgen, the others will be allowed when binding to fields. Configure them to
|
||||
# your liking
|
||||
models:
|
||||
ID:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.ID
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
- github.com/99designs/gqlgen/graphql.Uint
|
||||
- github.com/99designs/gqlgen/graphql.Uint64
|
||||
- github.com/99designs/gqlgen/graphql.Uint32
|
||||
Int:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
- github.com/99designs/gqlgen/graphql.Uint
|
||||
- github.com/99designs/gqlgen/graphql.Uint64
|
||||
- github.com/99designs/gqlgen/graphql.Uint32
|
||||
Time:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Time
|
||||
Meta:
|
||||
model:
|
||||
- github.com/sour-is/ev/pkg/es/event.Meta
|
||||
|
35
internal/graph/generated/federation.go
Normal file
35
internal/graph/generated/federation.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/99designs/gqlgen/plugin/federation/fedruntime"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownType = errors.New("unknown type")
|
||||
ErrTypeNotFound = errors.New("type not found")
|
||||
)
|
||||
|
||||
func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.Service, error) {
|
||||
if ec.DisableIntrospection {
|
||||
return fedruntime.Service{}, errors.New("federated introspection disabled")
|
||||
}
|
||||
|
||||
var sdl []string
|
||||
|
||||
for _, src := range sources {
|
||||
if src.BuiltIn {
|
||||
continue
|
||||
}
|
||||
sdl = append(sdl, src.Input)
|
||||
}
|
||||
|
||||
return fedruntime.Service{
|
||||
SDL: strings.Join(sdl, "\n"),
|
||||
}, nil
|
||||
}
|
4546
internal/graph/generated/generated.go
Normal file
4546
internal/graph/generated/generated.go
Normal file
File diff suppressed because it is too large
Load Diff
3
internal/graph/model/models_gen.go
Normal file
3
internal/graph/model/models_gen.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package model
|
50
internal/graph/resolver.go
Normal file
50
internal/graph/resolver.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/sour-is/ev/api/gql_ev"
|
||||
"github.com/sour-is/ev/internal/graph/generated"
|
||||
)
|
||||
|
||||
// This file will not be regenerated automatically.
|
||||
//
|
||||
// It serves as dependency injection for your app, add any dependencies you require here.
|
||||
|
||||
type Resolver struct {
|
||||
*gql_ev.Resolver
|
||||
}
|
||||
|
||||
func New(r *gql_ev.Resolver) *Resolver {
|
||||
return &Resolver{r}
|
||||
}
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||
|
||||
// Subscription returns generated.SubscriptionResolver implementation.
|
||||
// func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} }
|
||||
|
||||
type queryResolver struct{ *Resolver }
|
||||
|
||||
// type subscriptionResolver struct{ *Resolver }
|
||||
|
||||
func (r *Resolver) ChainMiddlewares(h http.Handler) http.Handler {
|
||||
v := reflect.ValueOf(r) // Get reflected value of *Resolver
|
||||
v = reflect.Indirect(v) // Get the pointed value (returns a zero value on nil)
|
||||
n := v.NumField() // Get number of fields to iterate over.
|
||||
for i := 0; i < n; i++ {
|
||||
f := v.Field(i)
|
||||
if !f.CanInterface() { // Skip non-interface types.
|
||||
continue
|
||||
}
|
||||
if iface, ok := f.Interface().(interface {
|
||||
GetMiddleware() func(http.Handler) http.Handler
|
||||
}); ok {
|
||||
h = iface.GetMiddleware()(h) // Append only items that fulfill the interface.
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
150
main.go
150
main.go
|
@ -1,25 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/sour-is/ev/api/gql_ev"
|
||||
"github.com/sour-is/ev/internal/graph"
|
||||
"github.com/sour-is/ev/internal/graph/generated"
|
||||
"github.com/sour-is/ev/pkg/es"
|
||||
"github.com/sour-is/ev/pkg/es/driver"
|
||||
diskstore "github.com/sour-is/ev/pkg/es/driver/disk-store"
|
||||
memstore "github.com/sour-is/ev/pkg/es/driver/mem-store"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
"github.com/sour-is/ev/pkg/es/service"
|
||||
"github.com/sour-is/ev/pkg/playground"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -34,10 +33,6 @@ func main() {
|
|||
}
|
||||
}
|
||||
func run(ctx context.Context) error {
|
||||
if err := event.Register(ctx, &PostEvent{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diskstore.Init(ctx)
|
||||
memstore.Init(ctx)
|
||||
|
||||
|
@ -46,15 +41,20 @@ func run(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
svc := &service{
|
||||
es: es,
|
||||
svc, err := service.New(ctx, es)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res := graph.New(gql_ev.New(es))
|
||||
gql := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: res}))
|
||||
|
||||
s := http.Server{
|
||||
Addr: env("EV_HTTP", ":8080"),
|
||||
}
|
||||
|
||||
http.HandleFunc("/event/", svc.event)
|
||||
http.Handle("/", playground.Handler("GraphQL playground", "/gql"))
|
||||
http.Handle("/gql", res.ChainMiddlewares(gql))
|
||||
http.Handle("/event/", http.StripPrefix("/event/", svc))
|
||||
|
||||
log.Print("Listen on ", s.Addr)
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
@ -65,7 +65,6 @@ func run(ctx context.Context) error {
|
|||
<-ctx.Done()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
log.Print("shutdown http")
|
||||
return s.Shutdown(ctx)
|
||||
})
|
||||
|
||||
|
@ -78,122 +77,3 @@ func env(name, defaultValue string) string {
|
|||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
type service struct {
|
||||
es driver.EventStore
|
||||
}
|
||||
|
||||
func (s *service) event(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var name, tags string
|
||||
if strings.HasPrefix(r.URL.Path, "/event/") {
|
||||
name = strings.TrimPrefix(r.URL.Path, "/event/")
|
||||
name, tags, _ = strings.Cut(name, "/")
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
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: strings.Split(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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user