Created
November 12, 2019 10:39
-
-
Save zaydek-old/0d31ea3dd0972c2bd066f69b38a58bff to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"database/sql" | |
"encoding/base64" | |
"encoding/json" | |
"fmt" | |
"time" | |
graphql "github.com/graph-gophers/graphql-go" | |
_ "github.com/lib/pq" | |
) | |
const schemaString = ` | |
schema { | |
query: Query | |
} | |
type Query { | |
users(first: Int, after: ID): UsersConnection! | |
user(userID: ID!): User! | |
} | |
type User { | |
userID: ID! | |
metaCreatedAt: String! | |
username: String! | |
} | |
type UsersConnection { | |
totalCount: Int! | |
edges: [UsersEdge!]! | |
pageInfo: PageInfo! | |
} | |
type UsersEdge { | |
cursor: ID! | |
node: User! | |
} | |
type PageInfo { | |
hasNextPage: Boolean! | |
} | |
` | |
type User struct { | |
UserID graphql.ID | |
MetaCreatedAt time.Time | |
Username string | |
} | |
/* | |
* RootResolver | |
*/ | |
// var Rx = &RootResolver{} | |
type RootResolver struct{} | |
type UsersArgs struct { | |
First *int32 | |
After *graphql.ID | |
} | |
func (r *RootResolver) Users(args UsersArgs) (*UsersConnResolver, error) { | |
// NOTE: limit’s zero value is nil and after’s zero value | |
// is time.Time{}. | |
var limit *int32 | |
if args.First != nil && *args.First > 0 { | |
limit = args.First | |
} | |
after, err := decodeCursor(args.After) | |
if err != nil { | |
return nil, err | |
} | |
var users []*User | |
rows, err := DB.Query(` | |
SELECT | |
user_id, | |
meta_created_at, | |
username | |
FROM users | |
WHERE meta_created_at > $1 | |
ORDER BY meta_created_at | |
LIMIT $2 | |
`, after, limit) | |
// NOTE: Ignore sql.ErrNoRows: | |
if err == sql.ErrNoRows { | |
err = nil | |
} | |
if err != nil { | |
return nil, err | |
} | |
defer rows.Close() | |
for rows.Next() { | |
user := &User{} | |
err := rows.Scan(&user.UserID, &user.MetaCreatedAt, &user.Username) | |
if err != nil { | |
return nil, err | |
} | |
users = append(users, user) | |
} | |
err = rows.Err() | |
if err != nil { | |
return nil, err | |
} | |
var hasNextPage bool | |
err = DB.QueryRow(` | |
SELECT | |
COUNT(*) > $1 | |
FROM users | |
WHERE meta_created_at > $2 | |
`, len(users), after).Scan(&hasNextPage) | |
if err != nil { | |
return nil, err | |
} | |
return &UsersConnResolver{users, hasNextPage}, nil | |
} | |
func (r *RootResolver) User(args struct{ UserID string }) (*UserResolver, error) { | |
user := &User{} | |
err := DB.QueryRow(` | |
SELECT | |
user_id, | |
meta_created_at, | |
username | |
FROM users | |
WHERE user_id = $1 | |
`, args.UserID).Scan(&user.UserID, &user.MetaCreatedAt, &user.Username) | |
if err != nil { | |
return nil, err | |
} | |
return &UserResolver{user}, nil | |
} | |
/* | |
* UsersConnResolver | |
*/ | |
type UsersConnResolver struct { | |
users []*User | |
hasNextPage bool | |
} | |
func (r *UsersConnResolver) TotalCount() int32 { | |
return int32(len(r.users)) | |
} | |
func (r *UsersConnResolver) Edges() []*UsersEdgeResolver { | |
var edges []*UsersEdgeResolver | |
for _, user := range r.users { | |
edges = append(edges, &UsersEdgeResolver{user}) | |
} | |
return edges | |
} | |
func (r *UsersConnResolver) PageInfo() *PageInfoResolver { | |
return &PageInfoResolver{r.hasNextPage} | |
} | |
/* | |
* UsersEdgeResolver | |
*/ | |
type UsersEdgeResolver struct{ user *User } | |
func (r *UsersEdgeResolver) Cursor() graphql.ID { | |
return encodeCursor(r.user.MetaCreatedAt) | |
} | |
func (r *UsersEdgeResolver) Node() *UserResolver { | |
return &UserResolver{r.user} | |
} | |
/* | |
* UserResolver | |
*/ | |
type UserResolver struct{ user *User } | |
func (r *UserResolver) UserID() graphql.ID { | |
return r.user.UserID | |
} | |
const PostgresFmt = "2006-01-02 15:04:05.000000" | |
func (r *UserResolver) MetaCreatedAt() string { | |
return r.user.MetaCreatedAt.Format(PostgresFmt) | |
} | |
func (r *UserResolver) Username() string { | |
return r.user.Username | |
} | |
/* | |
* PageInfo | |
*/ | |
type PageInfoResolver struct{ hasNextPage bool } | |
func (r *PageInfoResolver) HasNextPage() bool { | |
return r.hasNextPage | |
} | |
/* | |
* Cursor | |
*/ | |
// decodeCursor decodes an opaque cursor: | |
// | |
// LTYyMTM1NTk2ODAwLjA= -> 0001-01-01 00:00:00 +0000 UTC | |
func decodeCursor(cursor *graphql.ID) (time.Time, error) { | |
if cursor == nil { | |
return time.Time{}, nil | |
} | |
decoded, err := base64.StdEncoding.DecodeString(string(*cursor)) | |
if err != nil { | |
return time.Time{}, err | |
} | |
var sec, nsec int64 | |
_, err = fmt.Sscanf(string(decoded), "%d.%d", &sec, &nsec) | |
if err != nil { | |
return time.Time{}, err | |
} | |
return time.Unix(sec, nsec), nil | |
} | |
// encodeCursor encodes a cursor: | |
// | |
// 0001-01-01 00:00:00 +0000 UTC -> LTYyMTM1NTk2ODAwLjA= | |
func encodeCursor(cursor time.Time) graphql.ID { | |
epoch := fmt.Sprintf("%d.%d", cursor.Unix(), cursor.Nanosecond()) | |
encoded := base64.StdEncoding.EncodeToString([]byte(epoch)) | |
return graphql.ID(encoded) | |
} | |
/* | |
* main | |
*/ | |
var Schema = graphql.MustParseSchema(schemaString, &RootResolver{}) | |
var DB *sql.DB | |
type JSON = map[string]interface{} | |
type Query struct { | |
OperationName string | |
Query string | |
Variables JSON | |
} | |
func must(err error, desc ...string) { | |
if err == nil { | |
return | |
} | |
prefix := "" | |
if len(desc) == 1 { | |
prefix = desc[0] + ": " | |
} | |
errStr := fmt.Sprintf("%s%s", prefix, err) | |
panic(errStr) | |
} | |
func main() { | |
var err error | |
DB, err = sql.Open("postgres", "postgres://zaydek@localhost/graph_gophers_users?sslmode=disable") | |
must(err) | |
err = DB.Ping() | |
must(err) | |
defer DB.Close() | |
q1 := Query{ | |
"User", | |
`query User($userID: ID!) { | |
user(userID: $userID) { | |
userID | |
metaCreatedAt | |
username | |
} | |
}`, | |
JSON{ | |
"userID": "u-2e521b", | |
}, | |
} | |
resp1 := Schema.Exec(context.TODO(), q1.Query, "", q1.Variables) | |
bstr1, err := json.MarshalIndent(resp1, "", "\t") | |
must(err) | |
fmt.Println(string(bstr1)) | |
q2 := Query{ | |
"Users", | |
`query Users { | |
users { | |
totalCount | |
edges { | |
cursor | |
node { | |
userID | |
username | |
} | |
} | |
pageInfo { | |
hasNextPage | |
} | |
} | |
}`, | |
nil, | |
} | |
resp2 := Schema.Exec(context.TODO(), q2.Query, "", q2.Variables) | |
bstr2, err := json.MarshalIndent(resp2, "", "\t") | |
must(err) | |
fmt.Println(string(bstr2)) | |
q3 := Query{ | |
"Users", | |
`query Users($first: Int, $after: ID) { | |
users(first: $first, after: $after) { | |
totalCount | |
edges { | |
cursor | |
node { | |
userID | |
username | |
} | |
} | |
pageInfo { | |
hasNextPage | |
} | |
} | |
}`, | |
JSON{ | |
"first": 1, | |
"after": "MTU3MzQ1MjgzMS42Mzg5ODIwMDA=", | |
}, | |
} | |
resp3 := Schema.Exec(context.TODO(), q3.Query, "", q3.Variables) | |
bstr3, err := json.MarshalIndent(resp3, "", "\t") | |
must(err) | |
fmt.Println(string(bstr3)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment