Skip to content
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

feat: Schema list API #1625

Merged
merged 6 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 58 additions & 2 deletions api/http/handlerfuncs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/multiformats/go-multihash"
"github.com/pkg/errors"

"github.com/sourcenetwork/defradb/client"
corecrdt "github.com/sourcenetwork/defradb/core/crdt"
"github.com/sourcenetwork/defradb/events"
)
Expand Down Expand Up @@ -155,9 +156,64 @@ func execGQLHandler(rw http.ResponseWriter, req *http.Request) {
sendJSON(req.Context(), rw, newGQLResult(result.GQL), http.StatusOK)
}

type collectionResponse struct {
Name string `json:"name"`
type fieldResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
}

type collectionResponse struct {
Name string `json:"name"`
ID string `json:"id"`
Fields []fieldResponse `json:"fields,omitempty"`
}

func listSchemaHandler(rw http.ResponseWriter, req *http.Request) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: I would be really nice to have a unit test for this handler to see what the expected output is and that we catch future changes to the schema structure. Ideally the error paths would also be tested. You can base yourself off of the other unit test for this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I added tests for the error path as well as the happy path.

db, err := dbFromContext(req.Context())
if err != nil {
handleErr(req.Context(), rw, err, http.StatusInternalServerError)
return
}

cols, err := db.GetAllCollections(req.Context())
if err != nil {
handleErr(req.Context(), rw, err, http.StatusInternalServerError)
return
}

colResp := make([]collectionResponse, len(cols))
for i, col := range cols {
var fields []fieldResponse
for _, field := range col.Schema().Fields {
if field.Name == "_key" || field.RelationType == client.Relation_Type_INTERNAL_ID {
continue // ignore generated fields
}
fieldRes := fieldResponse{
ID: field.ID.String(),
Name: field.Name,
}
if field.IsObjectArray() {
fieldRes.Kind = fmt.Sprintf("[%s]", field.Schema)
} else if field.IsObject() {
fieldRes.Kind = field.Schema
} else {
fieldRes.Kind = field.Kind.String()
}
fields = append(fields, fieldRes)
}
colResp[i] = collectionResponse{
Name: col.Name(),
ID: col.SchemaID(),
Fields: fields,
}
}

sendJSON(
req.Context(),
rw,
simpleDataResponse("collections", colResp),
http.StatusOK,
)
}

func loadSchemaHandler(rw http.ResponseWriter, req *http.Request) {
Expand Down
114 changes: 110 additions & 4 deletions api/http/handlerfuncs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,112 @@ mutation {
}
}

func TestListSchemaHandlerWithoutDB(t *testing.T) {
t.Cleanup(CleanupEnv)
env = "dev"

errResponse := ErrorResponse{}
testRequest(testOptions{
Testing: t,
DB: nil,
Method: "GET",
Path: SchemaPath,
ExpectedStatus: 500,
ResponseData: &errResponse,
})

assert.Contains(t, errResponse.Errors[0].Extensions.Stack, "no database available")
assert.Equal(t, http.StatusInternalServerError, errResponse.Errors[0].Extensions.Status)
assert.Equal(t, "Internal Server Error", errResponse.Errors[0].Extensions.HTTPError)
assert.Equal(t, "no database available", errResponse.Errors[0].Message)
}

func TestListSchemaHandlerWitNoError(t *testing.T) {
ctx := context.Background()
defra := testNewInMemoryDB(t, ctx)
defer defra.Close(ctx)

stmt := `
type user {
name: String
age: Int
verified: Boolean
points: Float
}
type group {
owner: user
members: [user]
}`

_, err := defra.AddSchema(ctx, stmt)
if err != nil {
t.Fatal(err)
}

resp := DataResponse{}
testRequest(testOptions{
Testing: t,
DB: defra,
Method: "GET",
Path: SchemaPath,
ExpectedStatus: 200,
ResponseData: &resp,
})

switch v := resp.Data.(type) {
case map[string]any:
assert.Equal(t, map[string]any{
"collections": []any{
map[string]any{
"name": "group",
"id": "bafkreieunyhcyupkdppyo2g4zcqtdxvj5xi4f422gp2jwene6ohndvcobe",
"fields": []any{
map[string]any{
"id": "1",
"kind": "[user]",
"name": "members",
},
map[string]any{
"id": "2",
"kind": "user",
"name": "owner",
},
},
},
map[string]any{
"name": "user",
"id": "bafkreigrucdl7x3lsa4xwgz2bn7lbqmiwkifnspgx7hlkpaal3o55325bq",
"fields": []any{
map[string]any{
"id": "1",
"kind": "Int",
"name": "age",
},
map[string]any{
"id": "2",
"kind": "String",
"name": "name",
},
map[string]any{
"id": "3",
"kind": "Float",
"name": "points",
},
map[string]any{
"id": "4",
"kind": "Boolean",
"name": "verified",
},
},
},
},
}, v)

default:
t.Fatalf("data should be of type map[string]any but got %T\n%v", resp.Data, v)
}
}

func TestLoadSchemaHandlerWithReadBodyError(t *testing.T) {
t.Cleanup(CleanupEnv)
env = "dev"
Expand All @@ -604,7 +710,7 @@ func TestLoadSchemaHandlerWithReadBodyError(t *testing.T) {
Testing: t,
DB: nil,
Method: "POST",
Path: SchemaLoadPath,
Path: SchemaPath,
Body: &mockReadCloser,
ExpectedStatus: 500,
ResponseData: &errResponse,
Expand Down Expand Up @@ -634,7 +740,7 @@ type user {
Testing: t,
DB: nil,
Method: "POST",
Path: SchemaLoadPath,
Path: SchemaPath,
Body: buf,
ExpectedStatus: 500,
ResponseData: &errResponse,
Expand Down Expand Up @@ -669,7 +775,7 @@ types user {
Testing: t,
DB: defra,
Method: "POST",
Path: SchemaLoadPath,
Path: SchemaPath,
Body: buf,
ExpectedStatus: 500,
ResponseData: &errResponse,
Expand Down Expand Up @@ -705,7 +811,7 @@ type user {
Testing: t,
DB: defra,
Method: "POST",
Path: SchemaLoadPath,
Path: SchemaPath,
Body: buf,
ExpectedStatus: 200,
ResponseData: &resp,
Expand Down
22 changes: 11 additions & 11 deletions api/http/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@ const (
Version string = "v0"
versionedAPIPath string = "/api/" + Version

RootPath string = versionedAPIPath + ""
PingPath string = versionedAPIPath + "/ping"
DumpPath string = versionedAPIPath + "/debug/dump"
BlocksPath string = versionedAPIPath + "/blocks"
GraphQLPath string = versionedAPIPath + "/graphql"
SchemaLoadPath string = versionedAPIPath + "/schema/load"
SchemaPatchPath string = versionedAPIPath + "/schema/patch"
IndexPath string = versionedAPIPath + "/index"
PeerIDPath string = versionedAPIPath + "/peerid"
RootPath string = versionedAPIPath + ""
PingPath string = versionedAPIPath + "/ping"
DumpPath string = versionedAPIPath + "/debug/dump"
BlocksPath string = versionedAPIPath + "/blocks"
GraphQLPath string = versionedAPIPath + "/graphql"
SchemaPath string = versionedAPIPath + "/schema"
IndexPath string = versionedAPIPath + "/index"
PeerIDPath string = versionedAPIPath + "/peerid"
)

func setRoutes(h *handler) *handler {
Expand All @@ -59,8 +58,9 @@ func setRoutes(h *handler) *handler {
h.Get(BlocksPath+"/{cid}", h.handle(getBlockHandler))
h.Get(GraphQLPath, h.handle(execGQLHandler))
h.Post(GraphQLPath, h.handle(execGQLHandler))
h.Post(SchemaLoadPath, h.handle(loadSchemaHandler))
h.Post(SchemaPatchPath, h.handle(patchSchemaHandler))
h.Get(SchemaPath, h.handle(listSchemaHandler))
h.Post(SchemaPath, h.handle(loadSchemaHandler))
h.Patch(SchemaPath, h.handle(patchSchemaHandler))
h.Post(IndexPath, h.handle(createIndexHandler))
h.Delete(IndexPath, h.handle(dropIndexHandler))
h.Get(IndexPath, h.handle(listIndexHandler))
Expand Down
1 change: 1 addition & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func NewDefraCommand(cfg *config.Config) DefraCommand {
)
schemaCmd.AddCommand(
MakeSchemaAddCommand(cfg),
MakeSchemaListCommand(cfg),
MakeSchemaPatchCommand(cfg),
)
indexCmd.AddCommand(
Expand Down
2 changes: 1 addition & 1 deletion cli/schema_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Learn more about the DefraDB GraphQL Schema Language on https://docs.source.netw
return errors.New("empty schema provided")
}

endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaLoadPath)
endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaPath)
if err != nil {
return errors.Wrap("join paths failed", err)
}
Expand Down
83 changes: 83 additions & 0 deletions cli/schema_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2023 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package cli

import (
"encoding/json"
"io"
"net/http"

"github.com/spf13/cobra"

httpapi "github.com/sourcenetwork/defradb/api/http"
"github.com/sourcenetwork/defradb/config"
"github.com/sourcenetwork/defradb/errors"
)

type schemaListResponse struct {
Data struct {
Collections []struct {
Name string `json:"name"`
ID string `json:"id"`
Fields []struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
} `json:"fields"`
} `json:"collections"`
} `json:"data"`
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
}

func MakeSchemaListCommand(cfg *config.Config) *cobra.Command {
var cmd = &cobra.Command{
Use: "list",
Short: "List schema types with their respective fields",
RunE: func(cmd *cobra.Command, args []string) (err error) {
endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaPath)
if err != nil {
return NewErrFailedToJoinEndpoint(err)
}

res, err := http.Get(endpoint.String())
if err != nil {
return NewErrFailedToSendRequest(err)
}
defer res.Body.Close() //nolint:errcheck

data, err := io.ReadAll(res.Body)
if err != nil {
return NewErrFailedToReadResponseBody(err)
}

var r schemaListResponse
if err := json.Unmarshal(data, &r); err != nil {
return NewErrFailedToUnmarshalResponse(err)
}
if len(r.Errors) > 0 {
return errors.New("failed to list schemas", errors.NewKV("errors", r.Errors))
}

for _, c := range r.Data.Collections {
cmd.Printf("type %s {\n", c.Name)
for _, f := range c.Fields {
cmd.Printf("\t%s: %s\n", f.Name, f.Kind)
}
cmd.Printf("}\n")
}

return nil
},
}
return cmd
}
8 changes: 6 additions & 2 deletions cli/schema_patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,16 @@ To learn more about the DefraDB GraphQL Schema Language, refer to https://docs.s
return ErrEmptyFile
}

endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaPatchPath)
endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaPath)
if err != nil {
return err
}

res, err := http.Post(endpoint.String(), "text", strings.NewReader(patch))
req, err := http.NewRequest(http.MethodPatch, endpoint.String(), strings.NewReader(patch))
if err != nil {
return NewErrFailedToSendRequest(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return NewErrFailedToSendRequest(err)
}
Expand Down
Loading