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: support csv in tuple delete #260

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
15 changes: 5 additions & 10 deletions cmd/tuple/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ package tuple
import (
"context"
"fmt"
"os"

"github.com/openfga/go-sdk/client"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/output"
"github.com/openfga/cli/internal/tuplefile"
)

// deleteCmd represents the delete command.
Expand All @@ -46,19 +45,15 @@ var deleteCmd = &cobra.Command{
if err != nil {
return fmt.Errorf("failed to parse file name due to %w", err)
}

if fileName != "" {
var tuples []client.ClientTupleKeyWithoutCondition

data, err := os.ReadFile(fileName)
clientTuples, err := tuplefile.ReadTupleFile(fileName)
if err != nil {
return fmt.Errorf("failed to read file %s due to %w", fileName, err)
}

err = yaml.Unmarshal(data, &tuples)
if err != nil {
return fmt.Errorf("failed to parse input tuples due to %w", err)
}

openfgaTuples := tuplefile.ClientTupleKeyToTupleKeyWithoutCondition(clientTuples)
maxTuplesPerWrite, err := cmd.Flags().GetInt("max-tuples-per-write")
if err != nil {
return fmt.Errorf("failed to parse max tuples per write due to %w", err)
Expand All @@ -70,7 +65,7 @@ var deleteCmd = &cobra.Command{
}

deleteRequest := client.ClientWriteRequest{
Deletes: tuples,
Deletes: openfgaTuples,
}
response, err := ImportTuples(fgaClient, deleteRequest, maxTuplesPerWrite, maxParallelRequests)
if err != nil {
Expand Down
196 changes: 196 additions & 0 deletions cmd/tuple/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package tuple

import (
"testing"

openfga "github.com/openfga/go-sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/openfga/cli/internal/tuplefile"
)

func TestDeleteTuplesFileData(t *testing.T) { //nolint:funlen
t.Parallel()

tests := []struct {
name string
file string
expectedTuples []openfga.TupleKeyWithoutCondition
expectedError string
}{
{
name: "it can correctly parse a csv file",
file: "testdata/tuples.csv",
expectedTuples: []openfga.TupleKeyWithoutCondition{
{
User: "user:anne",
Relation: "owner",
Object: "folder:product",
},
{
User: "folder:product",
Relation: "parent",
Object: "folder:product-2021",
},
{
User: "team:fga#member",
Relation: "viewer",
Object: "folder:product-2021",
},
},
},
{
name: "it can correctly parse a csv file regardless of columns order",
file: "testdata/tuples_other_columns_order.csv",
expectedTuples: []openfga.TupleKeyWithoutCondition{
{
User: "user:anne",
Relation: "owner",
Object: "folder:product",
},
{
User: "folder:product",
Relation: "parent",
Object: "folder:product-2021",
},
{
User: "team:fga#member",
Relation: "viewer",
Object: "folder:product-2021",
},
},
},
{
name: "it can correctly parse a csv file without optional fields",
file: "testdata/tuples_without_optional_fields.csv",
expectedTuples: []openfga.TupleKeyWithoutCondition{
{
User: "user:anne",
Relation: "owner",
Object: "folder:product",
},
{
User: "folder:product",
Relation: "parent",
Object: "folder:product-2021",
},
},
},
{
name: "it can correctly parse a csv file with condition_name header but no condition_context header",
file: "testdata/tuples_with_condition_name_but_no_condition_context.csv",
expectedTuples: []openfga.TupleKeyWithoutCondition{
{
User: "user:anne",
Relation: "owner",
Object: "folder:product",
},
{
User: "folder:product",
Relation: "parent",
Object: "folder:product-2021",
},
{
User: "team:fga#member",
Relation: "viewer",
Object: "folder:product-2021",
},
},
},
{
name: "it can correctly parse a json file",
file: "testdata/tuples.json",
expectedTuples: []openfga.TupleKeyWithoutCondition{
{
User: "user:anne",
Relation: "owner",
Object: "folder:product",
},
{
User: "folder:product",
Relation: "parent",
Object: "folder:product-2021",
},
{
User: "user:beth",
Relation: "viewer",
Object: "folder:product-2021",
},
},
},
{
name: "it can correctly parse a yaml file",
file: "testdata/tuples.yaml",
expectedTuples: []openfga.TupleKeyWithoutCondition{
{
User: "user:anne",
Relation: "owner",
Object: "folder:product",
},
{
User: "folder:product",
Relation: "parent",
Object: "folder:product-2021",
},
{
User: "user:beth",
Relation: "viewer",
Object: "folder:product-2021",
},
},
},
{
name: "it fails to parse a non-supported file format",
file: "testdata/tuples.toml",
expectedError: "failed to parse input tuples: unsupported file format \".toml\"",
},
{
name: "it fails to parse a csv file with wrong headers",
file: "testdata/tuples_wrong_headers.csv",
expectedError: "failed to parse input tuples: invalid header \"a\", valid headers are " +
"user_type,user_id,user_relation,relation,object_type,object_id,condition_name,condition_context",
},
{
name: "it fails to parse a csv file with missing required headers",
file: "testdata/tuples_missing_required_headers.csv",
expectedError: "failed to parse input tuples: csv header missing (\"object_id\")",
},
{
name: "it fails to parse a csv file with missing condition_name header when condition_context is present",
file: "testdata/tuples_missing_condition_name_header.csv",
expectedError: "failed to parse input tuples: missing \"condition_name\"" +
" header which is required when \"condition_context\" is present",
},
{
name: "it fails to parse an empty csv file",
file: "testdata/tuples_empty.csv",
expectedError: "failed to parse input tuples: failed to read csv headers: EOF",
},
{
name: "it fails to parse a csv file with invalid rows",
file: "testdata/tuples_with_invalid_rows.csv",
expectedError: "failed to parse input tuples: failed to read tuple from csv file:" +
" record on line 2: wrong number of fields",
},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()

actualTuples, err := tuplefile.ReadTupleFile(test.file)
deleteTuples := tuplefile.ClientTupleKeyToTupleKeyWithoutCondition(actualTuples)

if test.expectedError != "" {
require.EqualError(t, err, test.expectedError)

return
}

require.NoError(t, err)
assert.Equal(t, test.expectedTuples, deleteTuples)
})
}
}
2 changes: 1 addition & 1 deletion cmd/tuple/testdata/tuples.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
user_type,user_id,user_relation,relation,object_type,object_id,condition_name,condition_context
user,anne,,owner,folder,product,inOfficeIP,
folder,product,,parent,folder,product-2021,inOfficeIP,"{""ip_addr"":""10.0.0.1""}"
team,fga,member,viewer,folder,product-2021,,
team,fga,member,viewer,folder,product-2021,,
17 changes: 17 additions & 0 deletions internal/tuplefile/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path"

openfga "github.com/openfga/go-sdk"
"github.com/openfga/go-sdk/client"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -32,3 +33,19 @@ func ReadTupleFile(fileName string) ([]client.ClientTupleKey, error) {

return tuples, nil
}

func ClientTupleKeyToTupleKeyWithoutCondition(clientTupleKey []client.ClientTupleKey,
) []openfga.TupleKeyWithoutCondition {
tuples := make([]openfga.TupleKeyWithoutCondition, 0, len(clientTupleKey))

for _, tuple := range clientTupleKey {
convertedTuple := openfga.TupleKeyWithoutCondition{
User: tuple.User,
Relation: tuple.Relation,
Object: tuple.Object,
}
tuples = append(tuples, convertedTuple)
}

return tuples
}