Work towards subscriptions

- Replace chi with gorilla/mux
- Write custom CORS rules
- Start on notification subscriptions
This commit is contained in:
viktorstrate
2020-02-21 16:50:50 +01:00
parent b3f4264b3d
commit b2a8fd09f9
12 changed files with 787 additions and 34 deletions

View File

@@ -9,7 +9,7 @@
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceRoot}/server/server.go",
"program": "${workspaceRoot}/server.go",
"cwd": "${workspaceRoot}",
"env": {},
"args": []

View File

@@ -8,6 +8,7 @@ require (
github.com/go-chi/cors v1.0.0
github.com/go-sql-driver/mysql v1.5.0
github.com/golang-migrate/migrate v3.5.4+incompatible
github.com/gorilla/mux v1.7.4
github.com/h2non/filetype v1.0.10
github.com/joho/godotenv v1.3.0
github.com/lib/pq v1.3.0

View File

@@ -4,6 +4,7 @@ github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+s
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
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/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
@@ -15,7 +16,10 @@ github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.1 h1:KOwqsTYZdeuMacU7CxjMNYEKeBvLbxW+psodrbcEa3A=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/h2non/filetype v1.0.10 h1:z+SJfnL6thYJ9kAST+6nPRXp1lMxnOVbMZHNYHMar0s=
@@ -30,6 +34,7 @@ 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/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/nf/cr2 v0.0.0-20180623103828-4699471a17ed h1:QP63yO3XEt8tJ1DgBsNjbOyBGjy2eHy9ITK4Eisr9rg=
github.com/nf/cr2 v0.0.0-20180623103828-4699471a17ed/go.mod h1:HazDB3gS/i//QXMMRmTAV7Ni9gAi4mDNTH2HjZ5aVgU=
@@ -39,6 +44,7 @@ github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKw
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
@@ -51,6 +57,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=

View File

@@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"io"
"strconv"
"sync"
"sync/atomic"
@@ -42,6 +43,7 @@ type ResolverRoot interface {
Photo() PhotoResolver
Query() QueryResolver
ShareToken() ShareTokenResolver
Subscription() SubscriptionResolver
}
type DirectiveRoot struct {
@@ -81,6 +83,16 @@ type ComplexityRoot struct {
UpdateUser func(childComplexity int, id int, username *string, rootPath *string, admin *bool) int
}
Notification struct {
Content func(childComplexity int) int
Header func(childComplexity int) int
Key func(childComplexity int) int
Negative func(childComplexity int) int
Positive func(childComplexity int) int
Progress func(childComplexity int) int
Type func(childComplexity int) int
}
Photo struct {
Album func(childComplexity int) int
Downloads func(childComplexity int) int
@@ -149,6 +161,10 @@ type ComplexityRoot struct {
InitialSetup func(childComplexity int) int
}
Subscription struct {
Notification func(childComplexity int) int
}
User struct {
Admin func(childComplexity int) int
ID func(childComplexity int) int
@@ -203,6 +219,9 @@ type ShareTokenResolver interface {
Album(ctx context.Context, obj *models.ShareToken) (*models.Album, error)
Photo(ctx context.Context, obj *models.ShareToken) (*models.Photo, error)
}
type SubscriptionResolver interface {
Notification(ctx context.Context) (<-chan *models.Notification, error)
}
type executableSchema struct {
resolvers ResolverRoot
@@ -440,6 +459,55 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.UpdateUser(childComplexity, args["id"].(int), args["username"].(*string), args["rootPath"].(*string), args["admin"].(*bool)), true
case "Notification.content":
if e.complexity.Notification.Content == nil {
break
}
return e.complexity.Notification.Content(childComplexity), true
case "Notification.header":
if e.complexity.Notification.Header == nil {
break
}
return e.complexity.Notification.Header(childComplexity), true
case "Notification.key":
if e.complexity.Notification.Key == nil {
break
}
return e.complexity.Notification.Key(childComplexity), true
case "Notification.negative":
if e.complexity.Notification.Negative == nil {
break
}
return e.complexity.Notification.Negative(childComplexity), true
case "Notification.positive":
if e.complexity.Notification.Positive == nil {
break
}
return e.complexity.Notification.Positive(childComplexity), true
case "Notification.progress":
if e.complexity.Notification.Progress == nil {
break
}
return e.complexity.Notification.Progress(childComplexity), true
case "Notification.type":
if e.complexity.Notification.Type == nil {
break
}
return e.complexity.Notification.Type(childComplexity), true
case "Photo.album":
if e.complexity.Photo.Album == nil {
break
@@ -778,6 +846,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.SiteInfo.InitialSetup(childComplexity), true
case "Subscription.notification":
if e.complexity.Subscription.Notification == nil {
break
}
return e.complexity.Subscription.Notification(childComplexity), true
case "User.admin":
if e.complexity.User.Admin == nil {
break
@@ -845,7 +920,36 @@ func (e *executableSchema) Mutation(ctx context.Context, op *ast.OperationDefini
}
func (e *executableSchema) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response {
return graphql.OneShot(graphql.ErrorResponse(ctx, "subscriptions are not supported"))
ec := executionContext{graphql.GetRequestContext(ctx), e}
next := ec._Subscription(ctx, op.SelectionSet)
if ec.Errors != nil {
return graphql.OneShot(&graphql.Response{Data: []byte("null"), Errors: ec.Errors})
}
var buf bytes.Buffer
return func() *graphql.Response {
buf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {
buf.Reset()
data := next()
if data == nil {
return nil
}
data.MarshalGQL(&buf)
return buf.Bytes()
})
if buf == nil {
return nil
}
return &graphql.Response{
Data: buf,
Errors: ec.Errors,
Extensions: ec.Extensions,
}
}
}
type executionContext struct {
@@ -955,6 +1059,25 @@ type Mutation {
deleteUser(id: Int!): User @isAdmin
}
type Subscription {
notification: Notification!
}
enum NotificationType {
Message
Progress
}
type Notification {
key: String!
type: NotificationType!
header: String!
content: String!
progress: Float
positive: Boolean!
negative: Boolean!
}
type AuthorizeResult {
success: Boolean!
status: String!
@@ -2506,6 +2629,262 @@ func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field grap
return ec.marshalOUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
}
func (ec *executionContext) _Notification_key(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Notification",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Key, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _Notification_type(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Notification",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Type, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(models.NotificationType)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNNotificationType2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotificationType(ctx, field.Selections, res)
}
func (ec *executionContext) _Notification_header(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Notification",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Header, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _Notification_content(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Notification",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Content, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _Notification_progress(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Notification",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Progress, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*float64)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalOFloat2ᚖfloat64(ctx, field.Selections, res)
}
func (ec *executionContext) _Notification_positive(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Notification",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Positive, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Notification_negative(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Notification",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Negative, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Photo_id(ctx context.Context, field graphql.CollectedField, obj *models.Photo) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
@@ -4220,6 +4599,52 @@ func (ec *executionContext) _SiteInfo_initialSetup(ctx context.Context, field gr
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Subscription_notification(ctx context.Context, field graphql.CollectedField) (ret func() graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Subscription",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Subscription().Notification(rctx)
})
if err != nil {
ec.Error(ctx, err)
return nil
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return nil
}
return func() graphql.Marshaler {
res, ok := <-resTmp.(<-chan *models.Notification)
if !ok {
return nil
}
return graphql.WriterFunc(func(w io.Writer) {
w.Write([]byte{'{'})
graphql.MarshalString(field.Alias).MarshalGQL(w)
w.Write([]byte{':'})
ec.marshalNNotification2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotification(ctx, field.Selections, res).MarshalGQL(w)
w.Write([]byte{'}'})
})
}
}
func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
@@ -5789,6 +6214,60 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
return out
}
var notificationImplementors = []string{"Notification"}
func (ec *executionContext) _Notification(ctx context.Context, sel ast.SelectionSet, obj *models.Notification) graphql.Marshaler {
fields := graphql.CollectFields(ec.RequestContext, sel, notificationImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("Notification")
case "key":
out.Values[i] = ec._Notification_key(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "type":
out.Values[i] = ec._Notification_type(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "header":
out.Values[i] = ec._Notification_header(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "content":
out.Values[i] = ec._Notification_content(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "progress":
out.Values[i] = ec._Notification_progress(ctx, field, obj)
case "positive":
out.Values[i] = ec._Notification_positive(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "negative":
out.Values[i] = ec._Notification_negative(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var photoImplementors = []string{"Photo"}
func (ec *executionContext) _Photo(ctx context.Context, sel ast.SelectionSet, obj *models.Photo) graphql.Marshaler {
@@ -6295,6 +6774,26 @@ func (ec *executionContext) _SiteInfo(ctx context.Context, sel ast.SelectionSet,
return out
}
var subscriptionImplementors = []string{"Subscription"}
func (ec *executionContext) _Subscription(ctx context.Context, sel ast.SelectionSet) func() graphql.Marshaler {
fields := graphql.CollectFields(ec.RequestContext, sel, subscriptionImplementors)
ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{
Object: "Subscription",
})
if len(fields) != 1 {
ec.Errorf(ctx, "must subscribe to exactly one stream")
return nil
}
switch fields[0].Name {
case "notification":
return ec._Subscription_notification(ctx, fields[0])
default:
panic("unknown field " + strconv.Quote(fields[0].Name))
}
}
var userImplementors = []string{"User"}
func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *models.User) graphql.Marshaler {
@@ -6675,6 +7174,29 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti
return res
}
func (ec *executionContext) marshalNNotification2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotification(ctx context.Context, sel ast.SelectionSet, v models.Notification) graphql.Marshaler {
return ec._Notification(ctx, sel, &v)
}
func (ec *executionContext) marshalNNotification2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotification(ctx context.Context, sel ast.SelectionSet, v *models.Notification) graphql.Marshaler {
if v == nil {
if !ec.HasError(graphql.GetResolverContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._Notification(ctx, sel, v)
}
func (ec *executionContext) unmarshalNNotificationType2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotificationType(ctx context.Context, v interface{}) (models.NotificationType, error) {
var res models.NotificationType
return res, res.UnmarshalGQL(v)
}
func (ec *executionContext) marshalNNotificationType2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotificationType(ctx context.Context, sel ast.SelectionSet, v models.NotificationType) graphql.Marshaler {
return v
}
func (ec *executionContext) marshalNPhoto2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhoto(ctx context.Context, sel ast.SelectionSet, v models.Photo) graphql.Marshaler {
return ec._Photo(ctx, sel, &v)
}

View File

@@ -22,6 +22,16 @@ type Filter struct {
Offset *int `json:"offset"`
}
type Notification struct {
Key string `json:"key"`
Type NotificationType `json:"type"`
Header string `json:"header"`
Content string `json:"content"`
Progress *float64 `json:"progress"`
Positive bool `json:"positive"`
Negative bool `json:"negative"`
}
type PhotoDownload struct {
Title string `json:"title"`
URL string `json:"url"`
@@ -63,6 +73,47 @@ type SiteInfo struct {
InitialSetup bool `json:"initialSetup"`
}
type NotificationType string
const (
NotificationTypeMessage NotificationType = "Message"
NotificationTypeProgress NotificationType = "Progress"
)
var AllNotificationType = []NotificationType{
NotificationTypeMessage,
NotificationTypeProgress,
}
func (e NotificationType) IsValid() bool {
switch e {
case NotificationTypeMessage, NotificationTypeProgress:
return true
}
return false
}
func (e NotificationType) String() string {
return string(e)
}
func (e *NotificationType) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = NotificationType(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid NotificationType", str)
}
return nil
}
func (e NotificationType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type OrderDirection string
const (

View File

@@ -0,0 +1,64 @@
package notification
import (
"errors"
"github.com/viktorstrate/photoview/api/graphql/models"
"log"
"sync"
)
type NotificationChannel = <-chan *models.Notification
type NotificationListener struct {
listenerID int
user models.User
channel NotificationChannel
}
func NewListener(user models.User, channel NotificationChannel) *NotificationListener {
nextNotificationId++
return &NotificationListener{
listenerID: nextNotificationId,
user: user,
channel: channel,
}
}
var notificationListeners []*NotificationListener = make([]*NotificationListener, 0)
var nextNotificationId = 0
var notificationLock = &sync.Mutex{}
func RegisterListener(user *models.User, channel NotificationChannel) int {
log.Println("Registering notification listener")
notificationLock.Lock()
defer notificationLock.Unlock()
notificationListeners = append(notificationListeners, NewListener(*user, channel))
return nextNotificationId
}
func DeregisterListener(listenerID int) error {
notificationLock.Lock()
defer notificationLock.Unlock()
for i, listener := range notificationListeners {
if listener.listenerID == listenerID {
if len(notificationListeners) > 1 {
lastIndex := len(notificationListeners) - 1
lastListener := notificationListeners[lastIndex]
notificationListeners[i] = lastListener
notificationListeners[lastIndex] = nil
notificationListeners = notificationListeners[:lastIndex]
} else {
notificationListeners = make([]*NotificationListener, 0)
}
return nil
}
}
return errors.New("ListenerID not found, while trying to deregister it")
}

View File

@@ -0,0 +1,27 @@
package resolvers
import (
"context"
"github.com/viktorstrate/photoview/api/graphql/auth"
"github.com/viktorstrate/photoview/api/graphql/notification"
)
func (r *subscriptionResolver) Notification(ctx context.Context) (notification.NotificationChannel, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
notificationChannel := make(notification.NotificationChannel, 1)
listenerID := notification.RegisterListener(user, notificationChannel)
go func() {
<-ctx.Done()
notification.DeregisterListener(listenerID)
}()
return notificationChannel, nil
}

View File

@@ -22,10 +22,20 @@ func (r *Resolver) Query() api.QueryResolver {
return &queryResolver{r}
}
func (r *Resolver) Subscription() api.SubscriptionResolver {
return &subscriptionResolver{
Resolver: r,
}
}
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type subscriptionResolver struct {
Resolver *Resolver
}
func (r *queryResolver) SiteInfo(ctx context.Context) (*models.SiteInfo, error) {
return models.GetSiteInfo(r.Database)
}

View File

@@ -85,6 +85,25 @@ type Mutation {
deleteUser(id: Int!): User @isAdmin
}
type Subscription {
notification: Notification!
}
enum NotificationType {
Message
Progress
}
type Notification {
key: String!
type: NotificationType!
header: String!
content: String!
progress: Float
positive: Boolean!
negative: Boolean!
}
type AuthorizeResult {
success: Boolean!
status: String!

View File

@@ -8,15 +8,16 @@ import (
"net/http"
"os"
"github.com/go-chi/chi"
"github.com/gorilla/mux"
"github.com/viktorstrate/photoview/api/graphql/auth"
"github.com/viktorstrate/photoview/api/graphql/models"
)
func PhotoRoutes(db *sql.DB) chi.Router {
router := chi.NewRouter()
router.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {
image_name := chi.URLParam(r, "name")
func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) {
image_name := mux.Vars(r)["name"]
row := db.QueryRow("SELECT photo_url.purpose, photo.path, photo.photo_id, photo.album_id, photo_url.content_type FROM photo_url, photo WHERE photo_url.photo_name = ? AND photo_url.photo_id = photo.photo_id", image_name)
@@ -129,6 +130,4 @@ func PhotoRoutes(db *sql.DB) chi.Router {
io.Copy(w, file)
})
return router
}

View File

@@ -7,14 +7,16 @@ import (
"os"
"path"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/cors"
"github.com/gorilla/mux"
// "github.com/go-chi/chi/middleware"
// "github.com/go-chi/cors"
"github.com/joho/godotenv"
"github.com/viktorstrate/photoview/api/database"
"github.com/viktorstrate/photoview/api/graphql/auth"
"github.com/viktorstrate/photoview/api/routes"
"github.com/viktorstrate/photoview/api/server"
"github.com/99designs/gqlgen/handler"
photoview_graphql "github.com/viktorstrate/photoview/api/graphql"
@@ -44,18 +46,20 @@ func main() {
log.Fatalf("Could not migrate database: %s\n", err)
}
router := chi.NewRouter()
router.Use(auth.Middleware(db))
rootRouter := mux.NewRouter()
rootRouter.Use(auth.Middleware(db))
router.Use(middleware.Logger)
// router.Use(middleware.Logger)
router.Use(cors.New(cors.Options{
AllowedOrigins: []string{"http://localhost:4001", "http://localhost:1234", "*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
AllowCredentials: true,
Debug: false,
}).Handler)
rootRouter.Use(server.CORSMiddleware(devMode))
// router.Use(cors.New(cors.Options{
// AllowedOrigins: []string{"http://localhost:4001", "http://localhost:1234", "*"},
// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
// AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
// AllowCredentials: true,
// Debug: false,
// }).Handler)
graphqlResolver := resolvers.Resolver{Database: db}
graphqlDirective := photoview_graphql.DirectiveRoot{}
@@ -72,19 +76,20 @@ func main() {
endpointURL, _ = url.Parse("/")
}
router.Route(endpointURL.Path, func(router chi.Router) {
endpointRouter := rootRouter.PathPrefix(endpointURL.Path).Subrouter()
if devMode {
router.Handle("/", handler.Playground("GraphQL playground", path.Join(endpointURL.Path, "/graphql")))
endpointRouter.Handle("/", handler.Playground("GraphQL playground", path.Join(endpointURL.Path, "/graphql")))
} else {
router.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
endpointRouter.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("photoview api endpoint"))
})
}
router.Handle("/graphql", handler.GraphQL(photoview_graphql.NewExecutableSchema(graphqlConfig), handler.IntrospectionEnabled(devMode)))
endpointRouter.Handle("/graphql", handler.GraphQL(photoview_graphql.NewExecutableSchema(graphqlConfig), handler.IntrospectionEnabled(devMode)))
router.Mount("/photo", routes.PhotoRoutes(db))
})
photoRouter := endpointRouter.PathPrefix("/photo").Subrouter()
routes.RegisterPhotoRoutes(db, photoRouter)
if devMode {
log.Printf("🚀 Graphql playground ready at %s", endpointURL.String())
@@ -92,5 +97,5 @@ func main() {
log.Printf("Photoview API endpoint available at %s", endpointURL.String())
}
log.Fatal(http.ListenAndServe(":"+port, router))
log.Fatal(http.ListenAndServe(":"+port, rootRouter))
}

View File

@@ -0,0 +1,48 @@
package server
import (
"log"
"net/http"
"net/url"
"os"
"path"
"strings"
"github.com/gorilla/mux"
)
func CORSMiddleware(devMode bool) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
methods := []string{http.MethodGet, http.MethodPost, http.MethodOptions}
headers := []string{"authorization", "content-type"}
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
endpoint, err := url.Parse(os.Getenv("API_ENDPOINT"))
if err != nil {
log.Fatalln("Could not parse API_ENDPOINT environment variable as url")
}
endpoint.Path = path.Join(endpoint.Path, "graphql")
if devMode {
// Development environment
w.Header().Set("Access-Control-Allow-Origin", req.Header.Get("origin"))
w.Header().Set("Vary", "Origin")
} else {
// Production environment
publicEndpoint, err := url.Parse(os.Getenv("PUBLIC_ENDPOINT"))
if err != nil {
log.Printf("Error parsing environment variable PUBLIC_ENDPOINT as url: %s", err)
} else {
w.Header().Set("Access-Control-Allow-Origin", publicEndpoint.Scheme+"://"+publicEndpoint.Host)
}
}
if req.Method != http.MethodOptions {
next.ServeHTTP(w, req)
}
})
}
}