Skip to content

Instantly share code, notes, and snippets.

@zaydek-old
Created November 12, 2019 10:39
Show Gist options
  • Save zaydek-old/0d31ea3dd0972c2bd066f69b38a58bff to your computer and use it in GitHub Desktop.
Save zaydek-old/0d31ea3dd0972c2bd066f69b38a58bff to your computer and use it in GitHub Desktop.
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