Skip to content

Prototype #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 52 commits into from
Mar 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f8c109b
Created a small graphql service using an example
mpenick Mar 5, 2020
cfb59f2
Added descriptions to the example to see the generated docs
mpenick Mar 5, 2020
c31ed97
Minor fixes
jorgebay Mar 5, 2020
7fbeaa5
Basic db access package
jorgebay Mar 5, 2020
5e21cdc
Basic SELECT working (with dynamic schema generation)
mpenick Mar 5, 2020
b2c138c
Extract queryFieldResolver and move it to schema package
jorgebay Mar 6, 2020
275f0e9
Basic insert mutation
jorgebay Mar 6, 2020
52d4d1f
Basic delete mutation and refactored query generation
jorgebay Mar 6, 2020
b8cba12
Fix: convert back to column names
jorgebay Mar 6, 2020
6f3ba1c
Use prepared statements and helper method
jorgebay Mar 6, 2020
28cab49
Simplify select query and fix naming conventions
mpenick Mar 6, 2020
6c9a9a6
Support for keyspace management
mpenick Mar 6, 2020
20e3b0c
Working on table management
mpenick Mar 6, 2020
6c5b5d9
Fix enums
mpenick Mar 7, 2020
cdb58b9
Table management work in progress
mpenick Mar 8, 2020
0cc4d25
Create table: adapt values from graphQL to gocql types
jorgebay Mar 9, 2020
016ef83
Basic REST API
jorgebay Mar 9, 2020
30698ee
Use scalars for uuid and timestamp
jorgebay Mar 10, 2020
bf3804b
Generalize deserializers to use TextUnmarshaler interface
jorgebay Mar 10, 2020
c50c293
Move graphql specific stuff out of the main package
mpenick Mar 11, 2020
b5cade4
Started working on live schema update
mpenick Mar 11, 2020
7e0d018
Added ignore for IDE settings and added a README.md file to work with.
Adron Mar 12, 2020
6468fca
Fix nullable column values
mpenick Mar 12, 2020
cc57b8d
Not setting values in map correctly
mpenick Mar 13, 2020
0cc6a10
Use a session interface to allow mocking
jorgebay Mar 13, 2020
f4f9835
Add support for a few more types
jorgebay Mar 16, 2020
e7fa416
Create GraphQL type definitions once and reuse them
jorgebay Mar 16, 2020
5c46257
Include result types
jorgebay Mar 16, 2020
5ff12ef
Integrate result types
jorgebay Mar 16, 2020
046f7f8
Mutations with input types
jorgebay Mar 17, 2020
f89c68f
Input type with relational and IN operators
jorgebay Mar 17, 2020
f327cb8
Return routes instead of handlers and encapsulate DB
mpenick Mar 17, 2020
5a82045
Add live updates for GraphQL schema generation (tables only)
mpenick Mar 17, 2020
524d26e
Add database plaintext authentication
mpenick Mar 17, 2020
c2db209
`NewEndpointConfig` is more descriptive
mpenick Mar 17, 2020
9d4996e
Left over name (typo)
mpenick Mar 17, 2020
7613e33
Introduce operators for SELECT queries
jorgebay Mar 17, 2020
3e16187
Simplify value scanning
jorgebay Mar 18, 2020
eb179aa
Support filter queries for tables
jorgebay Mar 18, 2020
b8cad57
Support ORDER BY in query generation
jorgebay Mar 18, 2020
c64503d
Change package`datastax` to `endpoint`
mpenick Mar 18, 2020
40d81a6
Add executeAs to the DB layer
mpenick Mar 17, 2020
4158616
Unit test for SELECT query generation
jorgebay Mar 19, 2020
bab920b
Unit test for INSERT query generation
jorgebay Mar 19, 2020
66052a3
ResultSet interface to simplify mocking and testing (#4)
jorgebay Mar 19, 2020
976682f
Use ConditionItem struct for both WHERE and IF clauses
jorgebay Mar 19, 2020
24a41cb
Move naming conventions to an interface
mpenick Mar 19, 2020
3f69da4
UPDATE query generation
jorgebay Mar 20, 2020
9b1d03b
Support update mutations in graphql
jorgebay Mar 20, 2020
7c42f27
Ignore tables with unsupported types
jorgebay Mar 23, 2020
97a0cb5
Support CQL inet type
jorgebay Mar 23, 2020
7920db0
Support CQL bigint and decimal types
jorgebay Mar 23, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
.idea
.DS_Store

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
data-endpoints

# Test binary, built with `go test -c`
*.test
Expand Down
Empty file added README.md
Empty file.
58 changes: 58 additions & 0 deletions config/naming.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package config

import "github.com/iancoleman/strcase"

type NamingConvention interface {
// ToCQLColumn converts a GraphQL/REST name to a CQL column name.
ToCQLColumn(name string) string

// ToCQLColumn converts a GraphQL/REST name to a CQL table name.
ToCQLTable(name string) string

// ToGraphQLField converts a CQL name (typically a column name) to a GraphQL field name.
ToGraphQLField(name string) string

// ToGraphQLOperation converts a CQL name (typically a table name) to a GraphQL operation name.
ToGraphQLOperation(prefix string, name string) string

// ToGraphQLType converts a CQL name (typically a table name) to a GraphQL type name.
ToGraphQLType(name string) string

// ToGraphQLEnumValue converts a CQL name to a GraphQL enumeration value name.
ToGraphQLEnumValue(name string) string
}

type defaultNaming struct{}

// Default naming implementation.
var DefaultNaming = &defaultNaming{}

func (n *defaultNaming) ToCQLColumn(name string) string {
// TODO: Fix numbers: "Table2" or "table2" --> "table_2"
return strcase.ToSnake(name)
}

func (n *defaultNaming) ToCQLTable(name string) string {
// TODO: Fix numbers: "Table2" or "table2" --> "table_2"
return strcase.ToSnake(name)
}

func (n *defaultNaming) ToGraphQLField(name string) string {
return strcase.ToLowerCamel(name)
}

func (n *defaultNaming) ToGraphQLOperation(prefix string, name string) string {
if prefix == "" {
return strcase.ToLowerCamel(name)
} else {
return strcase.ToLowerCamel(prefix) + strcase.ToCamel(name)
}
}

func (n *defaultNaming) ToGraphQLType(name string) string {
return strcase.ToCamel(name)
}

func (n *defaultNaming) ToGraphQLEnumValue(name string) string {
return strcase.ToCamel(name)
}
14 changes: 14 additions & 0 deletions data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"1": {
"id": "1",
"name": "Dan"
},
"2": {
"id": "2",
"name": "Lee"
},
"3": {
"id": "3",
"name": "Nick"
}
}
56 changes: 56 additions & 0 deletions db/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package db

import (
"github.com/gocql/gocql"
)

// Db represents a connection to a db
type Db struct {
session DbSession
}

// NewDb Gets a pointer to a db
func NewDb(username string, password string, hosts ...string) (*Db, error) {
cluster := gocql.NewCluster(hosts...)

if username != "" && password != "" {
cluster.Authenticator = gocql.PasswordAuthenticator{
Username: username,
Password: password,
}
}

var (
session *gocql.Session
err error
)

if session, err = cluster.CreateSession(); err != nil {
return nil, err
}

return &Db{
session: &GoCqlSession{ref: session},
}, nil
}

// Keyspace Retrieves a keyspace
func (db *Db) Keyspace(keyspace string) (*gocql.KeyspaceMetadata, error) {
// We expose gocql types for now, we should wrap them in the future instead
return db.session.KeyspaceMetadata(keyspace)
}

// Keyspaces Retrieves all the keyspace names
func (db *Db) Keyspaces() ([]string, error) {
iter, err := db.session.ExecuteIter("SELECT keyspace_name FROM system_schema.keyspaces", nil)
if err != nil {
return nil, err
}

var keyspaces []string
for _, row := range iter.Values() {
keyspaces = append(keyspaces, *row["keyspace_name"].(*string))
}

return keyspaces, nil
}
114 changes: 114 additions & 0 deletions db/db_session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package db

import (
"encoding/hex"
"github.com/gocql/gocql"
)

type QueryOptions struct {
UserOrRole string
Consistency gocql.Consistency
}

func NewQueryOptions() *QueryOptions {
return &QueryOptions{
Consistency: gocql.LocalOne,
}
}

func (q *QueryOptions) WithUserOrRole(userOrRole string) *QueryOptions {
q.UserOrRole = userOrRole
return q
}

func (q *QueryOptions) WithConsistency(userOrRole string) *QueryOptions {
q.UserOrRole = userOrRole
return q
}

type DbSession interface {
// Execute executes a statement without returning row results
Execute(query string, options *QueryOptions, values ...interface{}) error

// ExecuteIterSimple executes a statement and returns iterator to the result set
ExecuteIter(query string, options *QueryOptions, values ...interface{}) (ResultSet, error)

//TODO: Extract metadata methods from interface into another interface
KeyspaceMetadata(keyspaceName string) (*gocql.KeyspaceMetadata, error)
}

type ResultSet interface {
PageState() string
Values() []map[string]interface{}
}

func (r *goCqlResultIterator) PageState() string {
return hex.EncodeToString(r.pageState)
}

func (r *goCqlResultIterator) Values() []map[string]interface{} {
return r.values
}

type goCqlResultIterator struct {
pageState []byte
values []map[string]interface{}
}

func newResultIterator(iter *gocql.Iter) (*goCqlResultIterator, error) {
columns := iter.Columns()
scanner := iter.Scanner()

items := make([]map[string]interface{}, 0)

for scanner.Next() {
row, err := mapScan(scanner, columns)
if err != nil {
return nil, err
}
items = append(items, row)
}

if err := iter.Close(); err != nil {
return nil, err
}

return &goCqlResultIterator{
pageState: iter.PageState(),
values: items,
}, nil
}

type GoCqlSession struct {
ref *gocql.Session
}

func (db *Db) Execute(query string, options *QueryOptions, values ...interface{}) (ResultSet, error) {
return db.session.ExecuteIter(query, options, values...)
}

func (db *Db) ExecuteNoResult(query string, options *QueryOptions, values ...interface{}) error {
return db.session.Execute(query, options, values)
}

func (session *GoCqlSession) Execute(query string, options *QueryOptions, values ...interface{}) error {
_, err := session.ExecuteIter(query, options, values...)
return err
}

func (session *GoCqlSession) ExecuteIter(query string, options *QueryOptions, values ...interface{}) (ResultSet, error) {
q := session.ref.Query(query, values...)
if options != nil {
q.Consistency(options.Consistency)
if options.UserOrRole != "" {
q.CustomPayload(map[string][]byte{
"ProxyExecute": []byte(options.UserOrRole),
})
}
}
return newResultIterator(q.Iter())
}

func (session *GoCqlSession) KeyspaceMetadata(keyspaceName string) (*gocql.KeyspaceMetadata, error) {
return session.ref.KeyspaceMetadata(keyspaceName)
}
31 changes: 31 additions & 0 deletions db/keyspace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package db

import (
"fmt"
)

func (db *Db) CreateKeyspace(name string, dcReplicas map[string]int, options *QueryOptions) (bool, error) {
// TODO: Escape keyspace datacenter names?
dcs := ""
for name, replicas := range dcReplicas {
comma := ""
if len(dcs) > 0 {
comma = " ,"
}
dcs += fmt.Sprintf("%s'%s': %d", comma, name, replicas)
}

query := fmt.Sprintf("CREATE KEYSPACE %s WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', %s }", name, dcs)

err := db.session.Execute(query, options)

return err == nil, err
}

func (db *Db) DropKeyspace(name string, options *QueryOptions) (bool, error) {
// TODO: Escape keyspace name?
query := fmt.Sprintf("DROP KEYSPACE %s", name)
err := db.session.Execute(query, options)

return err == nil, err
}
Loading