Skip to content

Commit

Permalink
feat: Schema list API (#1625)
Browse files Browse the repository at this point in the history
## Relevant issue(s)

Resolves #1624 

## Description

This PR adds an API for listing schemas.
The schema API routes have also been simplified to be more RESTful.

### HTTP

`curl http://localhost:9181/api/v0/schema`

#### Output

```
{
  "data": {
    "collections": [
      {
        "name": "User",
        "id": "bafkreibpnvkvjqvg4skzlijka5xe63zeu74ivcjwd76q7yi65jdhwqhske",
        "fields": [
          {
            "id": "1",
            "name": "age",
            "kind": "Int"
          },
          {
            "id": "2",
            "name": "name",
            "kind": "String"
          },
          {
            "id": "3",
            "name": "points",
            "kind": "Float"
          },
          {
            "id": "4",
            "name": "verified",
            "kind": "Boolean"
          }
        ]
      }
    ],
    "result": "success"
  }
}
```

### CLI

`defradb client schema list`

#### Output

```
type User {
    name: String 
    age: Int 
    verified: Boolean 
    points: Float
}
```

## Tasks

- [x] I made sure the code is well commented, particularly
hard-to-understand areas.
- [x] I made sure the repository-held documentation is changed
accordingly.
- [x] I made sure the pull request title adheres to the conventional
commit style (the subset used in the project can be found in
[tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)).
- [x] I made sure to discuss its limitations such as threats to
validity, vulnerability to mistake and misuse, robustness to
invalidation of assumptions, resource requirements, ...

## How has this been tested?

Automated test + manual testing

Specify the platform(s) on which this was tested:
- MacOS
  • Loading branch information
nasdf authored Jul 12, 2023
1 parent 6978664 commit ac2fc49
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 20 deletions.
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) {
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

0 comments on commit ac2fc49

Please sign in to comment.