map[string]interface{}
, since it uses an approach based on strong typing. mkdir -p $GOPATH/src/github.com/ridhamtarpara/go-graphql-demo/
schema.graphql
) in the project root directory: type User { id: ID! name: String! email: String! } type Video { id: ID! name: String! description: String! user: User! url: String! createdAt: Timestamp! screenshots: [Screenshot] related(limit: Int = 25, offset: Int = 0): [Video!]! } type Screenshot { id: ID! videoId: ID! url: String! } input NewVideo { name: String! description: String! userId: ID! url: String! } type Mutation { createVideo(input: NewVideo!): Video! } type Query { Videos(limit: Int = 25, offset: Int = 0): [Video!]! } scalar Timestamp
Mutation
, a description of the data change request), which is used to publish new video files on the site, and one query ( Query
) to get a list of all video files. You can read more about GraphQL scheme here . In addition, here we declared one own scalar data type. The 5 standard scalar data types ( Int
, Float
, String
, Boolean
and ID
) that are in GraphQL are not enough.schema.graphql
(in our case, the type is Timestamp
) and provide their definitions in code. When using the gqlgen library, you need to provide marshaling and unmarshaling methods for all of your own scalar types and configure mapping with gqlgen.yml
.scripts/gqlgen.go
file scripts/gqlgen.go
following content: // +build ignore package main import "github.com/99designs/gqlgen/cmd" func main() { cmd.Execute() }
dep
: dep init
go run scripts/gqlgen.go init
gqlgen.yml
: configuration file for managing code generation.generated.go
: generated code.models_gen.go
: all models and data types of the provided schema.resolver.go
: here will be the code that the programmer creates.server/server.go
: entry point with http.Handler
to start GraphQL server.Video
type ( generated_video.go
file): type Video struct { ID string `json:"id"` Name string `json:"name"` User User `json:"user"` URL string `json:"url"` CreatedAt string `json:"createdAt"` Screenshots []*Screenshot `json:"screenshots"` Related []Video `json:"related"` }
ID
is a string, CreatedAt
is also a string. Other related models are configured accordingly. However, in real applications it is not necessary. If you use any type of SQL data, then you need, for example, that the ID
field would have an int
or int64
type, depending on the database used.ID
field to be of type int
, and the field of CreatedAt
is of type time.Time
. This leads to the fact that we need to define our own model and tell gqlgen to use our model instead of generating a new one. Here is the contents of the models.go
file: type Video struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` User User `json:"user"` URL string `json:"url"` CreatedAt time.Time `json:"createdAt"` Related []Video } // int ID func MarshalID(id int) graphql.Marshaler { return graphql.WriterFunc(func(w io.Writer) { io.WriteString(w, strconv.Quote(fmt.Sprintf("%d", id))) }) } // func UnmarshalID(v interface{}) (int, error) { id, ok := v.(string) if !ok { return 0, fmt.Errorf("ids must be strings") } i, e := strconv.Atoi(id) return int(i), e } func MarshalTimestamp(t time.Time) graphql.Marshaler { timestamp := t.Unix() * 1000 return graphql.WriterFunc(func(w io.Writer) { io.WriteString(w, strconv.FormatInt(timestamp, 10)) }) } func UnmarshalTimestamp(v interface{}) (time.Time, error) { if tmpStr, ok := v.(int); ok { return time.Unix(int64(tmpStr), 0), nil } return time.Time{}, errors.TimeStampError }
gqlgen.yml
file): schema: - schema.graphql exec: filename: generated.go model: filename: models_gen.go resolver: filename: resolver.go type: Resolver models: Video: model: github.com/ridhamtarpara/go-graphql-demo/api.Video ID: model: github.com/ridhamtarpara/go-graphql-demo/api.ID Timestamp: model: github.com/ridhamtarpara/go-graphql-demo/api.Timestamp
ID
and Timestamp
with marshaling and unmarshaling methods and their mapping in the gqlgen.yml
file. Now, when the user provides the string as an ID
, the UnmarshalID()
method converts the string to an integer. When sending a response, the MarshalID()
method converts the number to a string. The same happens with the Timestamp
or with any other scalar type declared by the programmer.resolver.go
file and enter descriptions of mutations and requests into it. There is already an automatically generated template code that we need to fill with meaning. Here is the code for this file: func (r *mutationResolver) CreateVideo(ctx context.Context, input NewVideo) (api.Video, error) { newVideo := api.Video{ URL: input.URL, Name: input.Name, CreatedAt: time.Now().UTC(), } rows, err := dal.LogAndQuery(r.db, "INSERT INTO videos (name, url, user_id, created_at) VALUES($1, $2, $3, $4) RETURNING id", input.Name, input.URL, input.UserID, newVideo.CreatedAt) defer rows.Close() if err != nil || !rows.Next() { return api.Video{}, err } if err := rows.Scan(&newVideo.ID); err != nil { errors.DebugPrintf(err) if errors.IsForeignKeyError(err) { return api.Video{}, errors.UserNotExist } return api.Video{}, errors.InternalServerError } return newVideo, nil } func (r *queryResolver) Videos(ctx context.Context, limit *int, offset *int) ([]api.Video, error) { var video api.Video var videos []api.Video rows, err := dal.LogAndQuery(r.db, "SELECT id, name, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2", limit, offset) defer rows.Close(); if err != nil { errors.DebugPrintf(err) return nil, errors.InternalServerError } for rows.Next() { if err := rows.Scan(&video.ID, &video.Name, &video.URL, &video.CreatedAt, &video.UserID); err != nil { errors.DebugPrintf(err) return nil, errors.InternalServerError } videos = append(videos, video) } return videos, nil }
user
object)? When working with GraphQL, concepts similar to “lazy” (lazy) and “greedy” (eager) loading are applicable. Since this system is expandable, you need to specify which fields need to be filled “greedily” and which ones are “lazy”.user
field (file models.go
): type Video struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` UserID int `json:"-"` URL string `json:"url"` CreatedAt time.Time `json:"createdAt"` }
UserID
and removed the User
structure. Now re-generate the code: go run scripts/gqlgen.go -v
generated.go
file): type VideoResolver interface { User(ctx context.Context, obj *api.Video) (api.User, error) Screenshots(ctx context.Context, obj *api.Video) ([]*api.Screenshot, error) Related(ctx context.Context, obj *api.Video, limit *int, offset *int) ([]api.Video, error) }
resolver.go
file): func (r *videoResolver) User(ctx context.Context, obj *api.Video) (api.User, error) { rows, _ := dal.LogAndQuery(r.db,"SELECT id, name, email FROM users where id = $1", obj.UserID) defer rows.Close() if !rows.Next() { return api.User{}, nil } var user api.User if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil { errors.DebugPrintf(err) return api.User{}, errors.InternalServerError } return user, nil }
schema.graphql
file. Here is the description of a subscription to a video posting event: type Subscription { videoPublished: Video! }
go run scripts/gqlgen.go -v
generated.go
file, an interface is created that needs to be implemented in the resolver. In our case it looks like this ( resolver.go
file): var videoPublishedChannel map[string]chan api.Video func init() { videoPublishedChannel = map[string]chan api.Video{} } type subscriptionResolver struct{ *Resolver } func (r *subscriptionResolver) VideoPublished(ctx context.Context) (<-chan api.Video, error) { id := randx.String(8) videoEvent := make(chan api.Video, 1) go func() { <-ctx.Done() }() videoPublishedChannel[id] = videoEvent return videoEvent, nil } func (r *mutationResolver) CreateVideo(ctx context.Context, input NewVideo) (api.Video, error) { // ... for _, observer := range videoPublishedChannel { observer <- newVideo } return newVideo, nil }
for _, observer := range videoPublishedChannel
line for _, observer := range videoPublishedChannel
.schema.graphql
file as follows: type Mutation { createVideo(input: NewVideo!): Video! @isAuthenticated } directive @isAuthenticated on FIELD_DEFINITION
isAuthenticated
directive and applied it to the createVideo
subscription. After the next session of automatic code generation, you need to define a definition for this directive. Now directives are implemented as methods of structures, and not as interfaces, so we need to describe them. I edited the automatically generated code in the server.go
file and created a method that returns the GraphQL configuration for the server.go
file. Here is the resolver.go
file: func NewRootResolvers(db *sql.DB) Config { c := Config{ Resolvers: &Resolver{ db: db, }, } // c.Directives.IsAuthenticated = func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { ctxUserID := ctx.Value(UserIDCtxKey) if ctxUserID != nil { return next(ctx) } else { return nil, errors.UnauthorisedError } } return c }
server.go
file: rootHandler:= dataloaders.DataloaderMiddleware( db, handler.GraphQL( go_graphql_demo.NewExecutableSchema(go_graphql_demo.NewRootResolvers(db) ) ) http.Handle("/query", auth.AuthMiddleware(rootHandler))
ID
from the context. Doesn't it seem strange to you? How did this meaning fit into the context and why did it even appear in the context? The fact is that gqlgen provides request contexts only at the implementation level, so we are not able to read any HTTP request data, such as headers or cookies, in recognizers or directives. As a result, you need to add your own intermediate mechanisms to the system, obtain this data and put it in context.ID
simply transmitted here. This mechanism is then integrated into server.go
with a new configuration loading method. directive @hasRole(role: Role!) on FIELD_DEFINITION enum Role { ADMIN USER }
query{ Videos(limit: 10){ name user{ name } } }
Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1
User
entity: go get github.com/vektah/dataloaden dataloaden github.com/ridhamtarpara/go-graphql-demo/api.User
userloader_gen.go
file userloader_gen.go
methods like Fetch
, LoadAll
and Prime
.Fetch
method ( dataloader.go
file) to get general results: func DataloaderMiddleware(db *sql.DB, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userloader := UserLoader{ wait : 1 * time.Millisecond, maxBatch: 100, fetch: func(ids []int) ([]*api.User, []error) { var sqlQuery string if len(ids) == 1 { sqlQuery = "SELECT id, name, email from users WHERE id = ?" } else { sqlQuery = "SELECT id, name, email from users WHERE id IN (?)" } sqlQuery, arguments, err := sqlx.In(sqlQuery, ids) if err != nil { log.Println(err) } sqlQuery = sqlx.Rebind(sqlx.DOLLAR, sqlQuery) rows, err := dal.LogAndQuery(db, sqlQuery, arguments...) defer rows.Close(); if err != nil { log.Println(err) } userById := map[int]*api.User{} for rows.Next() { user:= api.User{} if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil { errors.DebugPrintf(err) return nil, []error{errors.InternalServerError} } userById[user.ID] = &user } users := make([]*api.User, len(ids)) for i, id := range ids { users[i] = userById[id] i++ } return users, nil }, } ctx := context.WithValue(r.Context(), CtxKey, &userloader) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) }
resolver.go
file): func (r *videoResolver) User(ctx context.Context, obj *api.Video) (api.User, error) { user, err := ctx.Value(dataloaders.CtxKey).(*dataloaders.UserLoader).Load(obj.UserID) return *user, err }
Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2 Dataloader: User : SELECT id, name, email from users WHERE id IN ($1, $2, $3, $4, $5)
Video
, . GraphQL Video
. . — . { Videos(limit: 10, offset: 0){ name url related(limit: 10, offset: 0){ name url related(limit: 10, offset: 0){ name url related(limit: 100, offset: 0){ name url } } } } }
handler.ComplexityLimit(300)
) GraphQL (300 ). , ( server.go
): rootHandler:= dataloaders.DataloaderMiddleware( db, handler.GraphQL( go_graphql_demo.NewExecutableSchema(go_graphql_demo.NewRootResolvers(db)), handler.ComplexityLimit(300) ), )
resolver.go
: func NewRootResolvers(db *sql.DB) Config { c := Config{ Resolvers: &Resolver{ db: db, }, } // countComplexity := func(childComplexity int, limit *int, offset *int) int { return *limit * childComplexity } c.Complexity.Query.Videos = countComplexity c.Complexity.Video.Related = countComplexity // c.Directives.IsAuthenticated = func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { ctxUserID := ctx.Value(UserIDCtxKey) if ctxUserID != nil { return next(ctx) } else { return nil, errors.UnauthorisedError } } return c }
related
. , , , , .Source: https://habr.com/ru/post/444346/
All Articles