Repository: openfga/cli (go)

Issue:
Allow passing a set of relations to list relations

Pull request:
feat(query): allow restricting relations is ListRelations
<!-- Thanks for opening a PR! Here are some quick tips:
If this is your first time contributing, [read our Contributing Guidelines](https://github.com/openfga/.github/blob/main/CONTRIBUTING.md) to learn how to create an acceptable PR for this repo.
By submitting a PR to this repository, you agree to the terms within the [OpenFGA Code of Conduct](https://github.com/openfga/.github/blob/main/CODE_OF_CONDUCT.md)

If your PR is under active development, please submit it as a "draft". Once it's ready, open it up for review.
-->

<!-- Provide a brief summary of the changes -->

## Description
<!-- Provide a detailed description of the changes -->

<img width="1648" alt="Screenshot 2023-07-07 at 5 05 59 PM" src="https://github.com/openfga/cli/assets/536147/b7c1b2c3-a060-43ea-9e58-ab3b4e8b434a">


## References
<!-- Provide a list of any applicable references here (Github Issue, [OpenFGA RFC](https://github.com/openfga/rfcs), other PRs, etc..) -->
closes #46 


## Review Checklist
- [x] I have clicked on ["allow edits by maintainers"](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
- [x] I have added documentation for new/changed functionality in this PR or in a PR to [openfga.dev](https://github.com/openfga/openfga.dev) [Provide a link to any relevant PRs in the references section above]
- [x] The correct base branch is being used, if not `main`
- [x] I have added tests to validate that the change in functionality is working as expected


Patch:
diff --git a/README.md b/README.md
index de91f82..49d98aa 100644
--- a/README.md
+++ b/README.md
@@ -506,14 +506,13 @@ fga query **list-objects** <user> <object> [--relation <relation>]* [--contextua
 * `--contextual-tuple`: Contextual tuples (optional) (can be multiple)
 
 ###### Example
-`fga query list-relations --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne document:roadmap`
+`fga query list-relations --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne document:roadmap --relation can_view`
 
 ###### JSON Response
 ```json5
 {
     "relations": [
-      "can_view",
-      "can_edit"
+      "can_view"
     ],
 }
 ```
diff --git a/cmd/model/model.go b/cmd/model/model.go
index 7b4d30c..18aa8ea 100644
--- a/cmd/model/model.go
+++ b/cmd/model/model.go
@@ -16,9 +16,6 @@ limitations under the License.
 package model
 
 import (
-	"fmt"
-	"os"
-
 	"github.com/spf13/cobra"
 )
 
@@ -34,10 +31,4 @@ func init() {
 	ModelCmd.AddCommand(getCmd)
 	ModelCmd.AddCommand(validateCmd)
 	ModelCmd.PersistentFlags().String("store-id", "", "Store ID")
-
-	err := ModelCmd.MarkPersistentFlagRequired("store-id")
-	if err != nil {
-		fmt.Print(err)
-		os.Exit(1)
-	}
 }
diff --git a/cmd/query/list-relations.go b/cmd/query/list-relations.go
index 7f0d786..bfc047b 100644
--- a/cmd/query/list-relations.go
+++ b/cmd/query/list-relations.go
@@ -28,16 +28,14 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func listRelations(clientConfig fga.ClientConfig,
+func getRelationsForType(
+	clientConfig fga.ClientConfig,
 	fgaClient client.SdkClient,
-	user string,
-	objects string,
-	contextualTuples []client.ClientTupleKey,
-) (*client.ClientListRelationsResponse, error) {
+	object string,
+) (*[]string, error) {
 	var authorizationModel openfga.AuthorizationModel
 
 	if clientConfig.AuthorizationModelID != "" {
-		// note that the auth model id is already configured in the fgaClient.
 		response, err := fgaClient.ReadAuthorizationModel(context.Background()).Execute()
 		if err != nil {
 			return nil, fmt.Errorf("failed to list relations due to %w", err)
@@ -54,9 +52,8 @@ func listRelations(clientConfig fga.ClientConfig,
 	}
 
 	typeDefs := *(authorizationModel.TypeDefinitions)
-	objectType := strings.Split(objects, ":")[0]
-
-	var relations []string
+	objectType := strings.Split(object, ":")[0]
+	relations := []string{}
 
 	for index := range typeDefs {
 		if typeDefs[index].Type == objectType {
@@ -69,9 +66,28 @@ func listRelations(clientConfig fga.ClientConfig,
 		}
 	}
 
+	return &relations, nil
+}
+
+func listRelations(clientConfig fga.ClientConfig,
+	fgaClient client.SdkClient,
+	user string,
+	object string,
+	relations []string,
+	contextualTuples []client.ClientTupleKey,
+) (*client.ClientListRelationsResponse, error) {
+	if len(relations) < 1 {
+		relationsForType, err := getRelationsForType(clientConfig, fgaClient, object)
+		if err != nil {
+			return nil, fmt.Errorf("failed to list relations due to %w", err)
+		}
+
+		relations = *relationsForType
+	}
+
 	body := &client.ClientListRelationsRequest{
 		User:             user,
-		Object:           objects,
+		Object:           object,
 		Relations:        relations,
 		ContextualTuples: &contextualTuples,
 	}
@@ -95,8 +111,6 @@ var listRelationsCmd = &cobra.Command{
 		clientConfig := cmdutils.GetClientConfig(cmd)
 		fgaClient, err := clientConfig.GetFgaClient()
 		if err != nil {
-			fmt.Printf("Failed to initialize FGA Client due to %v", err)
-
 			return fmt.Errorf("failed to initialize FGA Client due to %w", err)
 		}
 
@@ -105,7 +119,9 @@ var listRelationsCmd = &cobra.Command{
 			return fmt.Errorf("error parsing contextual tuples for listRelations: %w", err)
 		}
 
-		response, err := listRelations(clientConfig, fgaClient, args[0], args[1], contextualTuples)
+		relations, _ := cmd.Flags().GetStringArray("relation")
+
+		response, err := listRelations(clientConfig, fgaClient, args[0], args[1], relations, contextualTuples)
 		if err != nil {
 			return fmt.Errorf("error listing relations: %w", err)
 		}
@@ -115,4 +131,5 @@ var listRelationsCmd = &cobra.Command{
 }
 
 func init() {
+	listRelationsCmd.Flags().StringArray("relation", []string{}, "Relation")
 }


Test patch:
diff --git a/cmd/query/list-relations_test.go b/cmd/query/list-relations_test.go
index 6fd9f4e..99c866e 100644
--- a/cmd/query/list-relations_test.go
+++ b/cmd/query/list-relations_test.go
@@ -33,10 +33,11 @@ func TestListRelationsLatestAuthModelError(t *testing.T) {
 
 	var clientConfig fga.ClientConfig
 
+	relations := []string{}
 	contextualTuples := []client.ClientTupleKey{
 		{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
 	}
-	_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples)
+	_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples)
 
 	if err == nil {
 		t.Error("Expect error but there is none")
@@ -61,10 +62,11 @@ func TestListRelationsAuthModelSpecifiedError(t *testing.T) {
 		AuthorizationModelID: "01GXSA8YR785C4FYS3C0RTG7B1",
 	}
 
+	relations := []string{}
 	contextualTuples := []client.ClientTupleKey{
 		{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
 	}
-	_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples)
+	_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples)
 
 	if err == nil {
 		t.Error("Expect error but there is none")
@@ -103,6 +105,7 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) {
 
 	mockBody := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl)
 
+	relations := []string{}
 	contextualTuples := []client.ClientTupleKey{
 		{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
 	}
@@ -120,7 +123,7 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) {
 
 	var clientConfig fga.ClientConfig
 
-	_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples)
+	_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples)
 	if err == nil {
 		t.Error("Expect error but there is none")
 	}
@@ -160,6 +163,7 @@ func TestListRelationsLatestAuthModelList(t *testing.T) {
 
 	mockBody := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl)
 
+	relations := []string{}
 	contextualTuples := []client.ClientTupleKey{
 		{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
 	}
@@ -177,7 +181,57 @@ func TestListRelationsLatestAuthModelList(t *testing.T) {
 
 	var clientConfig fga.ClientConfig
 
-	output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples)
+	output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples)
+	if err != nil {
+		t.Error(err)
+	}
+
+	if !reflect.DeepEqual(*output, expectedListRelationsResponse) {
+		t.Errorf("Expect output %v actual %v", expectedListRelationsResponse, *output)
+	}
+}
+
+func TestListRelationsMultipleRelations(t *testing.T) {
+	t.Parallel()
+
+	mockCtrl := gomock.NewController(t)
+	defer mockCtrl.Finish()
+	mockFgaClient := mock_client.NewMockSdkClient(mockCtrl)
+
+	// after reading the latest auth model, expect to call list relations but failure
+
+	mockListRelationsExecute := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl)
+
+	expectedListRelationsResponse := client.ClientListRelationsResponse{
+		Relations: []string{"viewer"},
+	}
+
+	mockListRelationsExecute.EXPECT().Execute().Return(&expectedListRelationsResponse, nil)
+
+	mockListRelationsRequest := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl)
+	listRelationsOptions := client.ClientListRelationsOptions{}
+	mockListRelationsRequest.EXPECT().Options(listRelationsOptions).Return(mockListRelationsExecute)
+
+	mockBody := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl)
+
+	relations := []string{"viewer", "editor"}
+	contextualTuples := []client.ClientTupleKey{
+		{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
+	}
+	body := client.ClientListRelationsRequest{
+		User:             "user:foo",
+		Relations:        []string{"viewer", "editor"},
+		Object:           "doc:doc1",
+		ContextualTuples: &contextualTuples,
+	}
+	mockBody.EXPECT().Body(body).Return(mockListRelationsRequest)
+	gomock.InOrder(
+		mockFgaClient.EXPECT().ListRelations(context.Background()).Return(mockBody),
+	)
+
+	var clientConfig fga.ClientConfig
+
+	output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples)
 	if err != nil {
 		t.Error(err)
 	}