
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