Skip to content

Commit 2f73c93

Browse files
committed
Use dynamic route with URL parameters
1 parent 6792d45 commit 2f73c93

File tree

7 files changed

+176
-59
lines changed

7 files changed

+176
-59
lines changed

config/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,12 @@ type Config interface {
1111
Naming() NamingConventionFn
1212
UseUserOrRoleAuth() bool
1313
Logger() log.Logger
14+
UrlPattern() UrlPattern
1415
}
16+
17+
type UrlPattern int
18+
19+
const (
20+
UrlPatternColon UrlPattern = iota
21+
UrlPatternBrackets
22+
)

config/mocks.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ func (o *ConfigMock) Logger() log.Logger {
4949
return args.Get(0).(log.Logger)
5050
}
5151

52+
func (o *ConfigMock) UrlPattern() UrlPattern {
53+
args := o.Called()
54+
return args.Get(0).(UrlPattern)
55+
}
56+
5257
type KeyspaceNamingInfoMock struct {
5358
mock.Mock
5459
}

endpoint/endpoint.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type DataEndpointConfig struct {
2020
naming config.NamingConventionFn
2121
useUserOrRoleAuth bool
2222
logger log.Logger
23+
urlPattern config.UrlPattern
2324
}
2425

2526
func (cfg DataEndpointConfig) ExcludedKeyspaces() []string {
@@ -42,6 +43,10 @@ func (cfg DataEndpointConfig) Logger() log.Logger {
4243
return cfg.logger
4344
}
4445

46+
func (cfg DataEndpointConfig) UrlPattern() config.UrlPattern {
47+
return cfg.urlPattern
48+
}
49+
4550
func (cfg *DataEndpointConfig) WithExcludedKeyspaces(ksExcluded []string) *DataEndpointConfig {
4651
cfg.ksExcluded = ksExcluded
4752
return cfg
@@ -72,6 +77,13 @@ func (cfg *DataEndpointConfig) WithDbPassword(dbPassword string) *DataEndpointCo
7277
return cfg
7378
}
7479

80+
// WithUrlPattern sets the url pattern to be use to separate url parameters
81+
// For example: "/graphql/:param1" (colon, default) or "/graphql/{param1}" (brackets)
82+
func (cfg *DataEndpointConfig) WithUrlPattern(pattern config.UrlPattern) *DataEndpointConfig {
83+
cfg.urlPattern = pattern
84+
return cfg
85+
}
86+
7587
func (cfg DataEndpointConfig) NewEndpoint() (*DataEndpoint, error) {
7688
dbClient, err := db.NewDb(cfg.dbUsername, cfg.dbPassword, cfg.dbHosts...)
7789
if err != nil {
@@ -104,15 +116,16 @@ func NewEndpointConfigWithLogger(logger log.Logger, hosts ...string) *DataEndpoi
104116
updateInterval: DefaultSchemaUpdateDuration,
105117
naming: config.NewDefaultNaming,
106118
logger: logger,
119+
urlPattern: config.UrlPatternColon,
107120
}
108121
}
109122

110123
func (e *DataEndpoint) RoutesGraphQL(pattern string) ([]graphql.Route, error) {
111-
return e.graphQLRouteGen.Routes(pattern)
124+
return e.graphQLRouteGen.Routes(pattern, "")
112125
}
113126

114127
func (e *DataEndpoint) RoutesKeyspaceGraphQL(pattern string, ksName string) ([]graphql.Route, error) {
115-
return e.graphQLRouteGen.RoutesKeyspace(pattern, ksName)
128+
return e.graphQLRouteGen.Routes(pattern, ksName)
116129
}
117130

118131
func (e *DataEndpoint) RoutesSchemaManagementGraphQL(pattern string, ops config.SchemaOperations) ([]graphql.Route, error) {

graphql/routes.go

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ package graphql
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"github.com/datastax/cassandra-data-apis/config"
89
"github.com/datastax/cassandra-data-apis/db"
910
"github.com/datastax/cassandra-data-apis/log"
1011
"github.com/graphql-go/graphql"
1112
"net/http"
1213
"path"
14+
"regexp"
15+
"strings"
1316
"time"
1417
)
1518

16-
type executeQueryFunc func(query string, ctx context.Context) *graphql.Result
19+
type executeQueryFunc func(query string, urlPath string, ctx context.Context) *graphql.Result
1720

1821
type RouteGenerator struct {
1922
dbClient *db.Db
2023
updateInterval time.Duration
2124
logger log.Logger
2225
schemaGen *SchemaGenerator
26+
urlPattern config.UrlPattern
2327
}
2428

2529
type Route struct {
@@ -42,60 +46,94 @@ func NewRouteGenerator(dbClient *db.Db, cfg config.Config) *RouteGenerator {
4246
updateInterval: cfg.SchemaUpdateInterval(),
4347
logger: cfg.Logger(),
4448
schemaGen: NewSchemaGenerator(dbClient, cfg),
49+
urlPattern: cfg.UrlPattern(),
4550
}
4651
}
4752

48-
func (rg *RouteGenerator) Routes(prefixPattern string) ([]Route, error) {
49-
ksNames, err := rg.dbClient.Keyspaces()
50-
if err != nil {
51-
return nil, fmt.Errorf("unable to retrieve keyspace names: %s", err)
52-
}
53-
54-
routes := make([]Route, 0, len(ksNames))
55-
56-
for _, ksName := range ksNames {
57-
if rg.schemaGen.isKeyspaceExcluded(ksName) {
58-
continue
59-
}
60-
ksRoutes, err := rg.RoutesKeyspace(path.Join(prefixPattern, ksName), ksName)
61-
if err != nil {
62-
return nil, err
63-
}
64-
routes = append(routes, ksRoutes...)
65-
}
66-
67-
return routes, nil
68-
}
69-
7053
func (rg *RouteGenerator) RoutesSchemaManagement(pattern string, ops config.SchemaOperations) ([]Route, error) {
7154
schema, err := rg.schemaGen.BuildKeyspaceSchema(ops)
7255
if err != nil {
7356
return nil, fmt.Errorf("unable to build graphql schema for schema management: %s", err)
7457
}
75-
return routesForSchema(pattern, func(query string, ctx context.Context) *graphql.Result {
58+
return routesForSchema(pattern, func(query string, urlPath string, ctx context.Context) *graphql.Result {
7659
return rg.executeQuery(query, ctx, schema)
7760
}), nil
7861
}
7962

80-
func (rg *RouteGenerator) RoutesKeyspace(pattern string, ksName string) ([]Route, error) {
81-
updater, err := NewUpdater(rg.schemaGen, ksName, rg.updateInterval, rg.logger)
63+
func (rg *RouteGenerator) Routes(pattern string, singleKeyspace string) ([]Route, error) {
64+
updater, err := NewUpdater(rg.schemaGen, singleKeyspace, rg.updateInterval, rg.logger)
8265
if err != nil {
83-
return nil, fmt.Errorf("unable to build graphql schema for keyspace '%s': %s", ksName, err)
66+
return nil, fmt.Errorf("unable to build graphql schema: %s", err)
8467
}
68+
8569
go updater.Start()
86-
return routesForSchema(pattern, func(query string, ctx context.Context) *graphql.Result {
87-
return rg.executeQuery(query, ctx, *updater.Schema())
70+
71+
pathParser := getPathParser(pattern)
72+
if singleKeyspace == "" {
73+
// Use a single route with keyspace as dynamic parameter
74+
switch rg.urlPattern {
75+
case config.UrlPatternColon:
76+
pattern = path.Join(pattern, ":keyspace")
77+
case config.UrlPatternBrackets:
78+
pattern = path.Join(pattern, "{keyspace}")
79+
default:
80+
return nil, errors.New("URL pattern not supported")
81+
}
82+
}
83+
84+
return routesForSchema(pattern, func(query string, urlPath string, ctx context.Context) *graphql.Result {
85+
ksName := singleKeyspace
86+
if ksName == "" {
87+
// Multiple keyspace support
88+
// The keyspace is part of the url path
89+
ksName = pathParser(urlPath)
90+
if ksName == "" {
91+
// Invalid url parameter
92+
return nil
93+
}
94+
}
95+
schema := updater.Schema(ksName)
96+
97+
if schema == nil {
98+
// The keyspace was not found or is invalid
99+
return nil
100+
}
101+
102+
return rg.executeQuery(query, ctx, *schema)
88103
}), nil
89104
}
90105

106+
func getPathParser(root string) func(string) string {
107+
if !strings.HasSuffix(root, "/") {
108+
root += "/"
109+
}
110+
regexString := fmt.Sprintf(`^%s([\w-]+)/?(?:\?.*)?$`, root)
111+
r := regexp.MustCompile(regexString)
112+
return func(urlPath string) string {
113+
subMatches := r.FindStringSubmatch(urlPath)
114+
if len(subMatches) != 2 {
115+
return ""
116+
}
117+
return subMatches[1]
118+
}
119+
}
120+
91121
func routesForSchema(pattern string, execute executeQueryFunc) []Route {
92122
return []Route{
93123
{
94124
Method: http.MethodGet,
95125
Pattern: pattern,
96126
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
97-
result := execute(r.URL.Query().Get("query"), r.Context())
98-
json.NewEncoder(w).Encode(result)
127+
result := execute(r.URL.Query().Get("query"), r.URL.Path, r.Context())
128+
if result == nil {
129+
// The execution function is signaling that it shouldn't be processing this request
130+
http.NotFound(w, r)
131+
return
132+
}
133+
err := json.NewEncoder(w).Encode(result)
134+
if err != nil {
135+
http.Error(w, "response could not be encoded: "+err.Error(), 500)
136+
}
99137
}),
100138
},
101139
{
@@ -114,8 +152,17 @@ func routesForSchema(pattern string, execute executeQueryFunc) []Route {
114152
return
115153
}
116154

117-
result := execute(body.Query, r.Context())
118-
json.NewEncoder(w).Encode(result)
155+
result := execute(body.Query, r.URL.Path, r.Context())
156+
if result == nil {
157+
// The execution function is signaling that it shouldn't be processing this request
158+
http.NotFound(w, r)
159+
return
160+
}
161+
162+
err = json.NewEncoder(w).Encode(result)
163+
if err != nil {
164+
http.Error(w, "response could not be encoded: "+err.Error(), 500)
165+
}
119166
}),
120167
},
121168
}

graphql/schema.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (sg *SchemaGenerator) buildQueriesFields(
6060
fields[ksSchema.naming.ToGraphQLOperation("", table.Name)] = &graphql.Field{
6161
Type: ksSchema.resultSelectTypes[table.Name],
6262
Args: graphql.FieldConfigArgument{
63-
"value": {Type: ksSchema.tableScalarInputTypes[table.Name]},
63+
"value": {Type: ksSchema.tableScalarInputTypes[table.Name]},
6464
"orderBy": {Type: graphql.NewList(ksSchema.orderEnums[table.Name])},
6565
"options": {Type: inputQueryOptions, DefaultValue: inputQueryOptionsDefault},
6666
},
@@ -115,7 +115,7 @@ func (sg *SchemaGenerator) buildMutationFields(
115115
fields[ksSchema.naming.ToGraphQLOperation(insertPrefix, name)] = &graphql.Field{
116116
Type: ksSchema.resultUpdateTypes[table.Name],
117117
Args: graphql.FieldConfigArgument{
118-
"value": {Type: graphql.NewNonNull(ksSchema.tableScalarInputTypes[table.Name])},
118+
"value": {Type: graphql.NewNonNull(ksSchema.tableScalarInputTypes[table.Name])},
119119
"ifNotExists": {Type: graphql.Boolean},
120120
"options": {Type: inputMutationOptions, DefaultValue: inputMutationOptionsDefault},
121121
},
@@ -125,7 +125,7 @@ func (sg *SchemaGenerator) buildMutationFields(
125125
fields[ksSchema.naming.ToGraphQLOperation(deletePrefix, name)] = &graphql.Field{
126126
Type: ksSchema.resultUpdateTypes[table.Name],
127127
Args: graphql.FieldConfigArgument{
128-
"value": {Type: graphql.NewNonNull(ksSchema.tableScalarInputTypes[table.Name])},
128+
"value": {Type: graphql.NewNonNull(ksSchema.tableScalarInputTypes[table.Name])},
129129
"ifExists": {Type: graphql.Boolean},
130130
"ifCondition": {Type: ksSchema.tableOperatorInputTypes[table.Name]},
131131
"options": {Type: inputMutationOptions, DefaultValue: inputMutationOptionsDefault},
@@ -136,7 +136,7 @@ func (sg *SchemaGenerator) buildMutationFields(
136136
fields[ksSchema.naming.ToGraphQLOperation(updatePrefix, name)] = &graphql.Field{
137137
Type: ksSchema.resultUpdateTypes[table.Name],
138138
Args: graphql.FieldConfigArgument{
139-
"value": {Type: graphql.NewNonNull(ksSchema.tableScalarInputTypes[table.Name])},
139+
"value": {Type: graphql.NewNonNull(ksSchema.tableScalarInputTypes[table.Name])},
140140
"ifExists": {Type: graphql.Boolean},
141141
"ifCondition": {Type: ksSchema.tableOperatorInputTypes[table.Name]},
142142
"options": {Type: inputMutationOptions, DefaultValue: inputMutationOptionsDefault},
@@ -169,15 +169,52 @@ func (sg *SchemaGenerator) buildMutation(
169169
})
170170
}
171171

172+
func (sg *SchemaGenerator) BuildSchemas(singleKeyspace string) (map[string]*graphql.Schema, error) {
173+
if singleKeyspace != "" {
174+
sg.logger.Info("building schema", "keyspace", singleKeyspace)
175+
// Schema generator is only focused on a single keyspace
176+
if schema, err := sg.buildSchema(singleKeyspace); err != nil {
177+
return nil, err
178+
} else {
179+
return map[string]*graphql.Schema{singleKeyspace: &schema}, nil
180+
}
181+
}
182+
183+
keyspaces, err := sg.dbClient.Keyspaces()
184+
if err != nil {
185+
return nil, err
186+
}
187+
188+
sg.logger.Info("building schemas")
189+
result := make(map[string]*graphql.Schema, len(keyspaces))
190+
builtKeyspaces := make([]string, 0, len(keyspaces))
191+
for _, ksName := range keyspaces {
192+
if sg.isKeyspaceExcluded(ksName) {
193+
continue
194+
}
195+
schema, err := sg.buildSchema(ksName)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
result[ksName] = &schema
201+
builtKeyspaces = append(builtKeyspaces, ksName)
202+
}
203+
204+
if len(builtKeyspaces) > 0 {
205+
sg.logger.Info("built keyspace schemas", "keyspaces", builtKeyspaces)
206+
}
207+
208+
return result, nil
209+
}
210+
172211
// Build GraphQL schema for tables in the provided keyspace metadata
173-
func (sg *SchemaGenerator) BuildSchema(keyspaceName string) (graphql.Schema, error) {
212+
func (sg *SchemaGenerator) buildSchema(keyspaceName string) (graphql.Schema, error) {
174213
keyspace, err := sg.dbClient.Keyspace(keyspaceName)
175214
if err != nil {
176215
return graphql.Schema{}, err
177216
}
178217

179-
sg.logger.Info("building schema", "keyspace", keyspace.Name)
180-
181218
ksNaming := sg.dbClient.KeyspaceNamingInfo(keyspace)
182219
keyspaceSchema := &KeyspaceGraphQLSchema{
183220
ignoredTables: make(map[string]bool),

0 commit comments

Comments
 (0)