diff --git a/cli/cli.go b/cli/cli.go index 0cb9fbb5bc..0f93b69633 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -59,6 +59,7 @@ func NewDefraCommand(cfg *config.Config) *cobra.Command { MakeSchemaAddCommand(), MakeSchemaPatchCommand(), MakeSchemaSetDefaultCommand(), + MakeSchemaDescribeCommand(), schema_migrate, ) diff --git a/cli/schema_describe.go b/cli/schema_describe.go new file mode 100644 index 0000000000..72d8eda474 --- /dev/null +++ b/cli/schema_describe.go @@ -0,0 +1,82 @@ +// 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 ( + "github.com/spf13/cobra" + + "github.com/sourcenetwork/defradb/client" +) + +func MakeSchemaDescribeCommand() *cobra.Command { + var name string + var root string + var versionID string + + var cmd = &cobra.Command{ + Use: "describe", + Short: "View schema descriptions.", + Long: `Introspect schema types. + +Example: view all schemas + defradb client schema describe + +Example: view schemas by name + defradb client schema describe --name User + +Example: view schemas by root + defradb client schema describe --root bae123 + +Example: view a single schema by version id + defradb client schema describe --version bae123 + `, + RunE: func(cmd *cobra.Command, args []string) error { + store := mustGetStoreContext(cmd) + + var schemas []client.SchemaDescription + switch { + case versionID != "": + schema, err := store.GetSchemaByVersionID(cmd.Context(), versionID) + if err != nil { + return err + } + return writeJSON(cmd, schema) + + case root != "": + s, err := store.GetSchemasByRoot(cmd.Context(), root) + if err != nil { + return err + } + schemas = s + + case name != "": + s, err := store.GetSchemasByName(cmd.Context(), name) + if err != nil { + return err + } + schemas = s + + default: + s, err := store.GetAllSchemas(cmd.Context()) + if err != nil { + return err + } + schemas = s + } + + return writeJSON(cmd, schemas) + }, + } + cmd.PersistentFlags().StringVar(&name, "name", "", "Schema name") + cmd.PersistentFlags().StringVar(&root, "root", "", "Schema root") + cmd.PersistentFlags().StringVar(&versionID, "version", "", "Schema Version ID") + return cmd +} diff --git a/client/db.go b/client/db.go index 81376bd6ca..b1b63f29d6 100644 --- a/client/db.go +++ b/client/db.go @@ -161,6 +161,22 @@ type Store interface { // this [Store]. GetAllCollections(context.Context) ([]Collection, error) + // GetSchemasByName returns the all schema versions with the given name. + GetSchemasByName(context.Context, string) ([]SchemaDescription, error) + + // GetSchemaByVersionID returns the schema description for the schema version of the + // ID provided. + // + // Will return an error if it is not found. + GetSchemaByVersionID(context.Context, string) (SchemaDescription, error) + + // GetSchemasByRoot returns the all schema versions for the given root. + GetSchemasByRoot(context.Context, string) ([]SchemaDescription, error) + + // GetAllSchemas returns all schema versions that currently exist within + // this [Store]. + GetAllSchemas(context.Context) ([]SchemaDescription, error) + // GetAllIndexes returns all the indexes that currently exist within this [Store]. GetAllIndexes(context.Context) (map[CollectionName][]IndexDescription, error) diff --git a/client/mocks/db.go b/client/mocks/db.go index f021d1fd10..df7b53fb5a 100644 --- a/client/mocks/db.go +++ b/client/mocks/db.go @@ -438,6 +438,60 @@ func (_c *DB_GetAllIndexes_Call) RunAndReturn(run func(context.Context) (map[str return _c } +// GetAllSchemas provides a mock function with given fields: _a0 +func (_m *DB) GetAllSchemas(_a0 context.Context) ([]client.SchemaDescription, error) { + ret := _m.Called(_a0) + + var r0 []client.SchemaDescription + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]client.SchemaDescription, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) []client.SchemaDescription); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]client.SchemaDescription) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetAllSchema_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllSchemas' +type DB_GetAllSchema_Call struct { + *mock.Call +} + +// GetAllSchemas is a helper method to define mock.On call +// - _a0 context.Context +func (_e *DB_Expecter) GetAllSchemas(_a0 interface{}) *DB_GetAllSchema_Call { + return &DB_GetAllSchema_Call{Call: _e.mock.On("GetAllSchemas", _a0)} +} + +func (_c *DB_GetAllSchema_Call) Run(run func(_a0 context.Context)) *DB_GetAllSchema_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *DB_GetAllSchema_Call) Return(_a0 []client.SchemaDescription, _a1 error) *DB_GetAllSchema_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetAllSchema_Call) RunAndReturn(run func(context.Context) ([]client.SchemaDescription, error)) *DB_GetAllSchema_Call { + _c.Call.Return(run) + return _c +} + // GetCollectionByName provides a mock function with given fields: _a0, _a1 func (_m *DB) GetCollectionByName(_a0 context.Context, _a1 string) (client.Collection, error) { ret := _m.Called(_a0, _a1) @@ -603,6 +657,169 @@ func (_c *DB_GetCollectionsByVersionID_Call) RunAndReturn(run func(context.Conte return _c } +// GetSchemasByName provides a mock function with given fields: _a0, _a1 +func (_m *DB) GetSchemasByName(_a0 context.Context, _a1 string) ([]client.SchemaDescription, error) { + ret := _m.Called(_a0, _a1) + + var r0 []client.SchemaDescription + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]client.SchemaDescription, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []client.SchemaDescription); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]client.SchemaDescription) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetSchemaByName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSchemasByName' +type DB_GetSchemaByName_Call struct { + *mock.Call +} + +// GetSchemasByName is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 string +func (_e *DB_Expecter) GetSchemasByName(_a0 interface{}, _a1 interface{}) *DB_GetSchemaByName_Call { + return &DB_GetSchemaByName_Call{Call: _e.mock.On("GetSchemasByName", _a0, _a1)} +} + +func (_c *DB_GetSchemaByName_Call) Run(run func(_a0 context.Context, _a1 string)) *DB_GetSchemaByName_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *DB_GetSchemaByName_Call) Return(_a0 []client.SchemaDescription, _a1 error) *DB_GetSchemaByName_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetSchemaByName_Call) RunAndReturn(run func(context.Context, string) ([]client.SchemaDescription, error)) *DB_GetSchemaByName_Call { + _c.Call.Return(run) + return _c +} + +// GetSchemasByRoot provides a mock function with given fields: _a0, _a1 +func (_m *DB) GetSchemasByRoot(_a0 context.Context, _a1 string) ([]client.SchemaDescription, error) { + ret := _m.Called(_a0, _a1) + + var r0 []client.SchemaDescription + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]client.SchemaDescription, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []client.SchemaDescription); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]client.SchemaDescription) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetSchemaByRoot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSchemasByRoot' +type DB_GetSchemaByRoot_Call struct { + *mock.Call +} + +// GetSchemasByRoot is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 string +func (_e *DB_Expecter) GetSchemasByRoot(_a0 interface{}, _a1 interface{}) *DB_GetSchemaByRoot_Call { + return &DB_GetSchemaByRoot_Call{Call: _e.mock.On("GetSchemasByRoot", _a0, _a1)} +} + +func (_c *DB_GetSchemaByRoot_Call) Run(run func(_a0 context.Context, _a1 string)) *DB_GetSchemaByRoot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *DB_GetSchemaByRoot_Call) Return(_a0 []client.SchemaDescription, _a1 error) *DB_GetSchemaByRoot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetSchemaByRoot_Call) RunAndReturn(run func(context.Context, string) ([]client.SchemaDescription, error)) *DB_GetSchemaByRoot_Call { + _c.Call.Return(run) + return _c +} + +// GetSchemaByVersionID provides a mock function with given fields: _a0, _a1 +func (_m *DB) GetSchemaByVersionID(_a0 context.Context, _a1 string) (client.SchemaDescription, error) { + ret := _m.Called(_a0, _a1) + + var r0 client.SchemaDescription + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (client.SchemaDescription, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, string) client.SchemaDescription); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(client.SchemaDescription) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_GetSchemaByVersionID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSchemaByVersionID' +type DB_GetSchemaByVersionID_Call struct { + *mock.Call +} + +// GetSchemaByVersionID is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 string +func (_e *DB_Expecter) GetSchemaByVersionID(_a0 interface{}, _a1 interface{}) *DB_GetSchemaByVersionID_Call { + return &DB_GetSchemaByVersionID_Call{Call: _e.mock.On("GetSchemaByVersionID", _a0, _a1)} +} + +func (_c *DB_GetSchemaByVersionID_Call) Run(run func(_a0 context.Context, _a1 string)) *DB_GetSchemaByVersionID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *DB_GetSchemaByVersionID_Call) Return(_a0 client.SchemaDescription, _a1 error) *DB_GetSchemaByVersionID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_GetSchemaByVersionID_Call) RunAndReturn(run func(context.Context, string) (client.SchemaDescription, error)) *DB_GetSchemaByVersionID_Call { + _c.Call.Return(run) + return _c +} + // LensRegistry provides a mock function with given fields: func (_m *DB) LensRegistry() client.LensRegistry { ret := _m.Called() diff --git a/db/description/schema.go b/db/description/schema.go index 832e614f32..06b129f3df 100644 --- a/db/description/schema.go +++ b/db/description/schema.go @@ -105,6 +105,48 @@ func GetSchemaVersion( return desc, nil } +// GetSchemasByName returns all the schema with the given name. +func GetSchemasByName( + ctx context.Context, + txn datastore.Txn, + name string, +) ([]client.SchemaDescription, error) { + allSchemas, err := GetAllSchemas(ctx, txn) + if err != nil { + return nil, err + } + + nameSchemas := []client.SchemaDescription{} + for _, schema := range allSchemas { + if schema.Name == name { + nameSchemas = append(nameSchemas, schema) + } + } + + return nameSchemas, nil +} + +// GetSchemasByRoot returns all the schema with the given root. +func GetSchemasByRoot( + ctx context.Context, + txn datastore.Txn, + root string, +) ([]client.SchemaDescription, error) { + allSchemas, err := GetAllSchemas(ctx, txn) + if err != nil { + return nil, err + } + + rootSchemas := []client.SchemaDescription{} + for _, schema := range allSchemas { + if schema.Root == root { + rootSchemas = append(rootSchemas, schema) + } + } + + return rootSchemas, nil +} + // GetSchemas returns the schema of all the default schema versions in the system. func GetSchemas( ctx context.Context, @@ -161,6 +203,47 @@ func GetSchemas( return descriptions, nil } +// GetSchemas returns all schema versions in the system. +func GetAllSchemas( + ctx context.Context, + txn datastore.Txn, +) ([]client.SchemaDescription, error) { + prefix := core.NewSchemaVersionKey("") + q, err := txn.Systemstore().Query(ctx, query.Query{ + Prefix: prefix.ToString(), + }) + if err != nil { + return nil, NewErrFailedToCreateSchemaQuery(err) + } + + schemas := make([]client.SchemaDescription, 0) + for res := range q.Next() { + if res.Error != nil { + if err := q.Close(); err != nil { + return nil, NewErrFailedToCloseSchemaQuery(err) + } + return nil, err + } + + var desc client.SchemaDescription + err = json.Unmarshal(res.Value, &desc) + if err != nil { + if err := q.Close(); err != nil { + return nil, NewErrFailedToCloseSchemaQuery(err) + } + return nil, err + } + + schemas = append(schemas, desc) + } + + if err := q.Close(); err != nil { + return nil, NewErrFailedToCloseSchemaQuery(err) + } + + return schemas, nil +} + func GetSchemaVersionIDs( ctx context.Context, txn datastore.Txn, diff --git a/db/schema.go b/db/schema.go index 627602119b..df95df60e2 100644 --- a/db/schema.go +++ b/db/schema.go @@ -289,6 +289,37 @@ func substituteSchemaPatch( return patch, nil } +func (db *db) getSchemasByName( + ctx context.Context, + txn datastore.Txn, + name string, +) ([]client.SchemaDescription, error) { + return description.GetSchemasByName(ctx, txn, name) +} + +func (db *db) getSchemaByVersionID( + ctx context.Context, + txn datastore.Txn, + versionID string, +) (client.SchemaDescription, error) { + return description.GetSchemaVersion(ctx, txn, versionID) +} + +func (db *db) getSchemasByRoot( + ctx context.Context, + txn datastore.Txn, + root string, +) ([]client.SchemaDescription, error) { + return description.GetSchemasByRoot(ctx, txn, root) +} + +func (db *db) getAllSchemas( + ctx context.Context, + txn datastore.Txn, +) ([]client.SchemaDescription, error) { + return description.GetAllSchemas(ctx, txn) +} + // getSubstituteFieldKind checks and attempts to get the underlying integer value for the given string // Field Kind value. It will return the value if one is found, else returns an [ErrFieldKindNotFound]. // diff --git a/db/txn_db.go b/db/txn_db.go index d9c1b4b206..380cfeed34 100644 --- a/db/txn_db.go +++ b/db/txn_db.go @@ -175,6 +175,78 @@ func (db *explicitTxnDB) GetAllCollections(ctx context.Context) ([]client.Collec return db.getAllCollections(ctx, db.txn) } +// GetSchemasByName returns the all schema versions with the given name. +func (db *implicitTxnDB) GetSchemasByName(ctx context.Context, name string) ([]client.SchemaDescription, error) { + txn, err := db.NewTxn(ctx, true) + if err != nil { + return nil, err + } + defer txn.Discard(ctx) + + return db.getSchemasByName(ctx, txn, name) +} + +// GetSchemasByName returns the all schema versions with the given name. +func (db *explicitTxnDB) GetSchemasByName(ctx context.Context, name string) ([]client.SchemaDescription, error) { + return db.getSchemasByName(ctx, db.txn, name) +} + +// GetSchemaByVersionID returns the schema description for the schema version of the +// ID provided. +// +// Will return an error if it is not found. +func (db *implicitTxnDB) GetSchemaByVersionID(ctx context.Context, versionID string) (client.SchemaDescription, error) { + txn, err := db.NewTxn(ctx, true) + if err != nil { + return client.SchemaDescription{}, err + } + defer txn.Discard(ctx) + + return db.getSchemaByVersionID(ctx, txn, versionID) +} + +// GetSchemaByVersionID returns the schema description for the schema version of the +// ID provided. +// +// Will return an error if it is not found. +func (db *explicitTxnDB) GetSchemaByVersionID(ctx context.Context, versionID string) (client.SchemaDescription, error) { + return db.getSchemaByVersionID(ctx, db.txn, versionID) +} + +// GetSchemasByRoot returns the all schema versions for the given root. +func (db *implicitTxnDB) GetSchemasByRoot(ctx context.Context, root string) ([]client.SchemaDescription, error) { + txn, err := db.NewTxn(ctx, true) + if err != nil { + return nil, err + } + defer txn.Discard(ctx) + + return db.getSchemasByRoot(ctx, txn, root) +} + +// GetSchemasByRoot returns the all schema versions for the given root. +func (db *explicitTxnDB) GetSchemasByRoot(ctx context.Context, root string) ([]client.SchemaDescription, error) { + return db.getSchemasByRoot(ctx, db.txn, root) +} + +// GetAllSchemas returns all schema versions that currently exist within +// this [Store]. +func (db *implicitTxnDB) GetAllSchemas(ctx context.Context) ([]client.SchemaDescription, error) { + txn, err := db.NewTxn(ctx, true) + if err != nil { + return nil, err + } + defer txn.Discard(ctx) + + return db.getAllSchemas(ctx, txn) +} + +// GetAllSchemas returns all schema versions that currently exist within +// this [Store]. +func (db *explicitTxnDB) GetAllSchemas(ctx context.Context) ([]client.SchemaDescription, error) { + return db.getAllSchemas(ctx, db.txn) +} + // GetAllIndexes gets all the indexes in the database. func (db *implicitTxnDB) GetAllIndexes( ctx context.Context, diff --git a/http/client.go b/http/client.go index 15c44aca38..148715e877 100644 --- a/http/client.go +++ b/http/client.go @@ -242,6 +242,65 @@ func (c *Client) GetAllCollections(ctx context.Context) ([]client.Collection, er return collections, nil } +func (c *Client) GetSchemasByName(ctx context.Context, name string) ([]client.SchemaDescription, error) { + methodURL := c.http.baseURL.JoinPath("schema") + methodURL.RawQuery = url.Values{"name": []string{name}}.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, methodURL.String(), nil) + if err != nil { + return nil, err + } + var schema []client.SchemaDescription + if err := c.http.requestJson(req, &schema); err != nil { + return nil, err + } + return schema, nil +} + +func (c *Client) GetSchemaByVersionID(ctx context.Context, versionID string) (client.SchemaDescription, error) { + methodURL := c.http.baseURL.JoinPath("schema") + methodURL.RawQuery = url.Values{"version_id": []string{versionID}}.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, methodURL.String(), nil) + if err != nil { + return client.SchemaDescription{}, err + } + var schema client.SchemaDescription + if err := c.http.requestJson(req, &schema); err != nil { + return client.SchemaDescription{}, err + } + return schema, nil +} + +func (c *Client) GetSchemasByRoot(ctx context.Context, root string) ([]client.SchemaDescription, error) { + methodURL := c.http.baseURL.JoinPath("schema") + methodURL.RawQuery = url.Values{"root": []string{root}}.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, methodURL.String(), nil) + if err != nil { + return nil, err + } + var schema []client.SchemaDescription + if err := c.http.requestJson(req, &schema); err != nil { + return nil, err + } + return schema, nil +} + +func (c *Client) GetAllSchemas(ctx context.Context) ([]client.SchemaDescription, error) { + methodURL := c.http.baseURL.JoinPath("schema") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, methodURL.String(), nil) + if err != nil { + return nil, err + } + var schema []client.SchemaDescription + if err := c.http.requestJson(req, &schema); err != nil { + return nil, err + } + return schema, nil +} + func (c *Client) GetAllIndexes(ctx context.Context) (map[client.CollectionName][]client.IndexDescription, error) { methodURL := c.http.baseURL.JoinPath("indexes") diff --git a/http/handler_store.go b/http/handler_store.go index 4a3bf0127c..aadbb37731 100644 --- a/http/handler_store.go +++ b/http/handler_store.go @@ -153,6 +153,41 @@ func (s *storeHandler) GetCollection(rw http.ResponseWriter, req *http.Request) } } +func (s *storeHandler) GetSchema(rw http.ResponseWriter, req *http.Request) { + store := req.Context().Value(storeContextKey).(client.Store) + + switch { + case req.URL.Query().Has("version_id"): + schema, err := store.GetSchemaByVersionID(req.Context(), req.URL.Query().Get("version_id")) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + responseJSON(rw, http.StatusOK, schema) + case req.URL.Query().Has("root"): + schema, err := store.GetSchemasByRoot(req.Context(), req.URL.Query().Get("root")) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + responseJSON(rw, http.StatusOK, schema) + case req.URL.Query().Has("name"): + schema, err := store.GetSchemasByName(req.Context(), req.URL.Query().Get("name")) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + responseJSON(rw, http.StatusOK, schema) + default: + schema, err := store.GetAllSchemas(req.Context()) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + responseJSON(rw, http.StatusOK, schema) + } +} + func (s *storeHandler) GetAllIndexes(rw http.ResponseWriter, req *http.Request) { store := req.Context().Value(storeContextKey).(client.Store) @@ -291,6 +326,9 @@ func (h *storeHandler) bindRoutes(router *Router) { collectionSchema := &openapi3.SchemaRef{ Ref: "#/components/schemas/collection", } + schemaSchema := &openapi3.SchemaRef{ + Ref: "#/components/schemas/schema", + } graphQLRequestSchema := &openapi3.SchemaRef{ Ref: "#/components/schemas/graphql_request", } @@ -411,6 +449,39 @@ func (h *storeHandler) bindRoutes(router *Router) { collectionDescribe.AddResponse(200, collectionsResponse) collectionDescribe.Responses["400"] = errorResponse + schemaNameQueryParam := openapi3.NewQueryParameter("name"). + WithDescription("Schema name"). + WithSchema(openapi3.NewStringSchema()) + schemaSchemaRootQueryParam := openapi3.NewQueryParameter("root"). + WithDescription("Schema root"). + WithSchema(openapi3.NewStringSchema()) + schemaVersionIDQueryParam := openapi3.NewQueryParameter("version_id"). + WithDescription("Schema version id"). + WithSchema(openapi3.NewStringSchema()) + + schemasSchema := openapi3.NewArraySchema() + schemasSchema.Items = schemaSchema + + schemaResponseSchema := openapi3.NewOneOfSchema() + schemaResponseSchema.OneOf = openapi3.SchemaRefs{ + schemaSchema, + openapi3.NewSchemaRef("", schemasSchema), + } + + schemaResponse := openapi3.NewResponse(). + WithDescription("Schema(s) with matching name, schema id, or version id."). + WithJSONSchema(schemaResponseSchema) + + schemaDescribe := openapi3.NewOperation() + schemaDescribe.OperationID = "schema_describe" + schemaDescribe.Description = "Introspect schema(s) by name, schema root, or version id." + schemaDescribe.Tags = []string{"schema"} + schemaDescribe.AddParameter(schemaNameQueryParam) + schemaDescribe.AddParameter(schemaSchemaRootQueryParam) + schemaDescribe.AddParameter(schemaVersionIDQueryParam) + schemaDescribe.AddResponse(200, schemaResponse) + schemaDescribe.Responses["400"] = errorResponse + graphQLRequest := openapi3.NewRequestBody(). WithContent(openapi3.NewContentWithJSONSchemaRef(graphQLRequestSchema)) @@ -455,5 +526,6 @@ func (h *storeHandler) bindRoutes(router *Router) { router.AddRoute("/debug/dump", http.MethodGet, debugDump, h.PrintDump) router.AddRoute("/schema", http.MethodPost, addSchema, h.AddSchema) router.AddRoute("/schema", http.MethodPatch, patchSchema, h.PatchSchema) + router.AddRoute("/schema", http.MethodGet, schemaDescribe, h.GetSchema) router.AddRoute("/schema/default", http.MethodPost, setDefaultSchemaVersion, h.SetDefaultSchemaVersion) } diff --git a/http/openapi.go b/http/openapi.go index 88a8f2097d..4aa217e939 100644 --- a/http/openapi.go +++ b/http/openapi.go @@ -29,6 +29,7 @@ var openApiSchemas = map[string]any{ "graphql_response": &GraphQLResponse{}, "backup_config": &client.BackupConfig{}, "collection": &client.CollectionDescription{}, + "schema": &client.SchemaDescription{}, "index": &client.IndexDescription{}, "delete_result": &client.DeleteResult{}, "update_result": &client.UpdateResult{}, diff --git a/tests/clients/cli/wrapper.go b/tests/clients/cli/wrapper.go index 0215d92d82..43c0aba820 100644 --- a/tests/clients/cli/wrapper.go +++ b/tests/clients/cli/wrapper.go @@ -281,6 +281,65 @@ func (w *Wrapper) GetAllCollections(ctx context.Context) ([]client.Collection, e return cols, err } +func (w *Wrapper) GetSchemasByName(ctx context.Context, name string) ([]client.SchemaDescription, error) { + args := []string{"client", "schema", "describe"} + args = append(args, "--name", name) + + data, err := w.cmd.execute(ctx, args) + if err != nil { + return nil, err + } + var schema []client.SchemaDescription + if err := json.Unmarshal(data, &schema); err != nil { + return nil, err + } + return schema, err +} + +func (w *Wrapper) GetSchemaByVersionID(ctx context.Context, versionID string) (client.SchemaDescription, error) { + args := []string{"client", "schema", "describe"} + args = append(args, "--version", versionID) + + data, err := w.cmd.execute(ctx, args) + if err != nil { + return client.SchemaDescription{}, err + } + var schema client.SchemaDescription + if err := json.Unmarshal(data, &schema); err != nil { + return client.SchemaDescription{}, err + } + return schema, err +} + +func (w *Wrapper) GetSchemasByRoot(ctx context.Context, root string) ([]client.SchemaDescription, error) { + args := []string{"client", "schema", "describe"} + args = append(args, "--root", root) + + data, err := w.cmd.execute(ctx, args) + if err != nil { + return nil, err + } + var schema []client.SchemaDescription + if err := json.Unmarshal(data, &schema); err != nil { + return nil, err + } + return schema, err +} + +func (w *Wrapper) GetAllSchemas(ctx context.Context) ([]client.SchemaDescription, error) { + args := []string{"client", "schema", "describe"} + + data, err := w.cmd.execute(ctx, args) + if err != nil { + return nil, err + } + var schema []client.SchemaDescription + if err := json.Unmarshal(data, &schema); err != nil { + return nil, err + } + return schema, err +} + func (w *Wrapper) GetAllIndexes(ctx context.Context) (map[client.CollectionName][]client.IndexDescription, error) { args := []string{"client", "index", "list"} diff --git a/tests/clients/http/wrapper.go b/tests/clients/http/wrapper.go index 3a26b6c0fd..ab7975a525 100644 --- a/tests/clients/http/wrapper.go +++ b/tests/clients/http/wrapper.go @@ -127,6 +127,22 @@ func (w *Wrapper) GetAllCollections(ctx context.Context) ([]client.Collection, e return w.client.GetAllCollections(ctx) } +func (w *Wrapper) GetSchemasByName(ctx context.Context, name string) ([]client.SchemaDescription, error) { + return w.client.GetSchemasByName(ctx, name) +} + +func (w *Wrapper) GetSchemaByVersionID(ctx context.Context, versionID string) (client.SchemaDescription, error) { + return w.client.GetSchemaByVersionID(ctx, versionID) +} + +func (w *Wrapper) GetSchemasByRoot(ctx context.Context, root string) ([]client.SchemaDescription, error) { + return w.client.GetSchemasByRoot(ctx, root) +} + +func (w *Wrapper) GetAllSchemas(ctx context.Context) ([]client.SchemaDescription, error) { + return w.client.GetAllSchemas(ctx) +} + func (w *Wrapper) GetAllIndexes(ctx context.Context) (map[client.CollectionName][]client.IndexDescription, error) { return w.client.GetAllIndexes(ctx) } diff --git a/tests/integration/schema/get_schema_test.go b/tests/integration/schema/get_schema_test.go new file mode 100644 index 0000000000..e6d5f166ac --- /dev/null +++ b/tests/integration/schema/get_schema_test.go @@ -0,0 +1,274 @@ +// 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 schema + +import ( + "testing" + + "github.com/sourcenetwork/immutable" + + "github.com/sourcenetwork/defradb/client" + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestGetSchema_GivenNonExistantSchemaVersionID_Errors(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.GetSchema{ + VersionID: immutable.Some("does not exist"), + ExpectedError: "datastore: key not found", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestGetSchema_GivenNoSchemaReturnsEmptySet(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.GetSchema{ + ExpectedResults: []client.SchemaDescription{}, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestGetSchema_GivenNoSchemaGivenUnknownRoot(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.GetSchema{ + Root: immutable.Some("does not exist"), + ExpectedResults: []client.SchemaDescription{}, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestGetSchema_GivenNoSchemaGivenUnknownName(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.GetSchema{ + Name: immutable.Some("does not exist"), + ExpectedResults: []client.SchemaDescription{}, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestGetSchema_ReturnsAllSchema(t *testing.T) { + usersSchemaVersion1ID := "bafkreickgf3nbjaairxkkqawmrv7fafaafyccl4qygqeveagisdn42eohu" + usersSchemaVersion2ID := "bafkreicseqwxooxo2wf2bgzdalwtm2rtsj7x4mgsir4rp4htmpnwnffwre" + booksSchemaVersion1ID := "bafkreigbfibfn7g6neen2gghc54dzocexefi7vshc3opgvy6j7jflar2nm" + + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users {} + `, + }, + testUtils.SchemaUpdate{ + Schema: ` + type Books {} + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Fields/-", "value": {"Name": "name", "Kind": "String"} } + ] + `, + SetAsDefaultVersion: immutable.Some(false), + }, + testUtils.GetSchema{ + ExpectedResults: []client.SchemaDescription{ + { + Name: "Users", + Root: usersSchemaVersion1ID, + VersionID: usersSchemaVersion1ID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + }, + }, + }, + { + Name: "Users", + Root: usersSchemaVersion1ID, + VersionID: usersSchemaVersion2ID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + Typ: client.LWW_REGISTER, + }, + { + Name: "name", + ID: 1, + Kind: client.FieldKind_STRING, + Typ: client.LWW_REGISTER, + }, + }, + }, + { + Name: "Books", + Root: booksSchemaVersion1ID, + VersionID: booksSchemaVersion1ID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + }, + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestGetSchema_ReturnsSchemaForGivenRoot(t *testing.T) { + usersSchemaVersion1ID := "bafkreickgf3nbjaairxkkqawmrv7fafaafyccl4qygqeveagisdn42eohu" + usersSchemaVersion2ID := "bafkreicseqwxooxo2wf2bgzdalwtm2rtsj7x4mgsir4rp4htmpnwnffwre" + + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users {} + `, + }, + testUtils.SchemaUpdate{ + Schema: ` + type Books {} + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Fields/-", "value": {"Name": "name", "Kind": "String"} } + ] + `, + SetAsDefaultVersion: immutable.Some(false), + }, + testUtils.GetSchema{ + Root: immutable.Some(usersSchemaVersion1ID), + ExpectedResults: []client.SchemaDescription{ + { + Name: "Users", + Root: usersSchemaVersion1ID, + VersionID: usersSchemaVersion1ID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + }, + }, + }, + { + Name: "Users", + Root: usersSchemaVersion1ID, + VersionID: usersSchemaVersion2ID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + Typ: client.LWW_REGISTER, + }, + { + Name: "name", + ID: 1, + Kind: client.FieldKind_STRING, + Typ: client.LWW_REGISTER, + }, + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestGetSchema_ReturnsSchemaForGivenName(t *testing.T) { + usersSchemaVersion1ID := "bafkreickgf3nbjaairxkkqawmrv7fafaafyccl4qygqeveagisdn42eohu" + usersSchemaVersion2ID := "bafkreicseqwxooxo2wf2bgzdalwtm2rtsj7x4mgsir4rp4htmpnwnffwre" + + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users {} + `, + }, + testUtils.SchemaUpdate{ + Schema: ` + type Books {} + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Fields/-", "value": {"Name": "name", "Kind": "String"} } + ] + `, + SetAsDefaultVersion: immutable.Some(false), + }, + testUtils.GetSchema{ + Name: immutable.Some("Users"), + ExpectedResults: []client.SchemaDescription{ + { + Name: "Users", + Root: usersSchemaVersion1ID, + VersionID: usersSchemaVersion1ID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + }, + }, + }, + { + Name: "Users", + Root: usersSchemaVersion1ID, + VersionID: usersSchemaVersion2ID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + Typ: client.LWW_REGISTER, + }, + { + Name: "name", + ID: 1, + Kind: client.FieldKind_STRING, + Typ: client.LWW_REGISTER, + }, + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/schema/simple_test.go b/tests/integration/schema/simple_test.go index 47ef9810be..6bcb2a1dec 100644 --- a/tests/integration/schema/simple_test.go +++ b/tests/integration/schema/simple_test.go @@ -13,10 +13,15 @@ package schema import ( "testing" + "github.com/sourcenetwork/immutable" + + "github.com/sourcenetwork/defradb/client" testUtils "github.com/sourcenetwork/defradb/tests/integration" ) func TestSchemaSimpleCreatesSchemaGivenEmptyType(t *testing.T) { + schemaVersionID := "bafkreickgf3nbjaairxkkqawmrv7fafaafyccl4qygqeveagisdn42eohu" + test := testUtils.TestCase{ Actions: []any{ testUtils.SchemaUpdate{ @@ -38,6 +43,22 @@ func TestSchemaSimpleCreatesSchemaGivenEmptyType(t *testing.T) { }, }, }, + testUtils.GetSchema{ + VersionID: immutable.Some(schemaVersionID), + ExpectedResults: []client.SchemaDescription{ + { + Name: "Users", + VersionID: schemaVersionID, + Root: schemaVersionID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + }, + }, + }, + }, + }, }, } diff --git a/tests/integration/schema/updates/add/field/simple_test.go b/tests/integration/schema/updates/add/field/simple_test.go index 56931567d4..69ddfd1734 100644 --- a/tests/integration/schema/updates/add/field/simple_test.go +++ b/tests/integration/schema/updates/add/field/simple_test.go @@ -15,10 +15,14 @@ import ( "github.com/sourcenetwork/immutable" + "github.com/sourcenetwork/defradb/client" testUtils "github.com/sourcenetwork/defradb/tests/integration" ) func TestSchemaUpdatesAddFieldSimple(t *testing.T) { + schemaVersion1ID := "bafkreih27vuxrj4j2tmxnibfm77wswa36xji74hwhq7deipj5rvh3qyabq" + schemaVersion2ID := "bafkreid5bpw7sipm63l5gxxjrs34yrq2ur5xrzyseez5rnj3pvnvkaya6m" + test := testUtils.TestCase{ Description: "Test schema update, add field", Actions: []any{ @@ -45,6 +49,35 @@ func TestSchemaUpdatesAddFieldSimple(t *testing.T) { }`, Results: []map[string]any{}, }, + testUtils.GetSchema{ + VersionID: immutable.Some(schemaVersion2ID), + ExpectedResults: []client.SchemaDescription{ + { + Name: "Users", + VersionID: schemaVersion2ID, + Root: schemaVersion1ID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + Typ: client.LWW_REGISTER, + }, + { + Name: "name", + ID: 1, + Kind: client.FieldKind_STRING, + Typ: client.LWW_REGISTER, + }, + { + Name: "email", + ID: 2, + Kind: client.FieldKind_STRING, + Typ: client.LWW_REGISTER, + }, + }, + }, + }, + }, }, } testUtils.ExecuteTestCase(t, test) @@ -83,6 +116,64 @@ func TestSchemaUpdates_AddFieldSimpleDoNotSetDefault_Errors(t *testing.T) { testUtils.ExecuteTestCase(t, test) } +func TestSchemaUpdates_AddFieldSimpleDoNotSetDefault_VersionIsQueryable(t *testing.T) { + schemaVersion1ID := "bafkreih27vuxrj4j2tmxnibfm77wswa36xji74hwhq7deipj5rvh3qyabq" + schemaVersion2ID := "bafkreid5bpw7sipm63l5gxxjrs34yrq2ur5xrzyseez5rnj3pvnvkaya6m" + + test := testUtils.TestCase{ + Description: "Test schema update, add field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Fields/-", "value": {"Name": "email", "Kind": 11} } + ] + `, + SetAsDefaultVersion: immutable.Some(false), + }, + testUtils.GetSchema{ + VersionID: immutable.Some(schemaVersion2ID), + ExpectedResults: []client.SchemaDescription{ + { + Name: "Users", + // Even though schema version 2 is not active, it should still be possible to + // fetch it. + VersionID: schemaVersion2ID, + Root: schemaVersion1ID, + Fields: []client.FieldDescription{ + { + Name: "_key", + Kind: client.FieldKind_DocKey, + Typ: client.LWW_REGISTER, + }, + { + Name: "name", + ID: 1, + Kind: client.FieldKind_STRING, + Typ: client.LWW_REGISTER, + }, + { + Name: "email", + ID: 2, + Kind: client.FieldKind_STRING, + Typ: client.LWW_REGISTER, + }, + }, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + func TestSchemaUpdatesAddFieldSimpleErrorsAddingToUnknownCollection(t *testing.T) { test := testUtils.TestCase{ Description: "Test schema update, add to unknown collection fails", diff --git a/tests/integration/test_case.go b/tests/integration/test_case.go index fabdccbbfd..112a497dc8 100644 --- a/tests/integration/test_case.go +++ b/tests/integration/test_case.go @@ -91,6 +91,31 @@ type SchemaPatch struct { ExpectedError string } +// GetSchema is an action that fetches schema using the provided options. +type GetSchema struct { + // NodeID may hold the ID (index) of a node to apply this patch to. + // + // If a value is not provided the patch will be applied to all nodes. + NodeID immutable.Option[int] + + // The VersionID of the schema version to fetch. + // + // This option will be prioritized over all other options. + VersionID immutable.Option[string] + + // The Root of the schema versions to fetch. + // + // This option will be prioritized over Name. + Root immutable.Option[string] + + // The Name of the schema versions to fetch. + Name immutable.Option[string] + + ExpectedResults []client.SchemaDescription + + ExpectedError string +} + // SetDefaultSchemaVersion is an action that will set the default schema version to the // given value. type SetDefaultSchemaVersion struct { diff --git a/tests/integration/utils2.go b/tests/integration/utils2.go index 01c6c7c69f..a9480c15ec 100644 --- a/tests/integration/utils2.go +++ b/tests/integration/utils2.go @@ -254,6 +254,9 @@ func performAction( case SchemaPatch: patchSchema(s, action) + case GetSchema: + getSchema(s, action) + case SetDefaultSchemaVersion: setDefaultSchemaVersion(s, action) @@ -949,6 +952,35 @@ func patchSchema( refreshIndexes(s) } +func getSchema( + s *state, + action GetSchema, +) { + for _, node := range getNodes(action.NodeID, s.nodes) { + var results []client.SchemaDescription + var err error + switch { + case action.VersionID.HasValue(): + result, e := node.GetSchemaByVersionID(s.ctx, action.VersionID.Value()) + err = e + results = []client.SchemaDescription{result} + case action.Root.HasValue(): + results, err = node.GetSchemasByRoot(s.ctx, action.Root.Value()) + case action.Name.HasValue(): + results, err = node.GetSchemasByName(s.ctx, action.Name.Value()) + default: + results, err = node.GetAllSchemas(s.ctx) + } + + expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) + + if !expectedErrorRaised { + require.Equal(s.t, action.ExpectedResults, results) + } + } +} + func setDefaultSchemaVersion( s *state, action SetDefaultSchemaVersion,