feat: add graphql

This commit is contained in:
Jon Lundy 2022-08-07 11:55:49 -06:00
parent f436393965
commit 82f23ae323
Signed by untrusted user who does not match committer: xuu
GPG Key ID: C63E6D61F3035024
20 changed files with 5234 additions and 170 deletions

View File

@ -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
View File

@ -0,0 +1 @@
package gql_ev

46
api/gql_ev/models.go Normal file
View 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
View 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
}

View 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
View File

@ -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
View File

@ -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
View 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

View 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
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model

View 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
View File

@ -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()
}

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)
}
}
}