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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user