Skip to content

Commit 7e724b5

Browse files
kindlyturnipskindlyturnips
kindlyturnips
authored andcommitted
feat: support csv in tuple delete
1 parent f8d2c52 commit 7e724b5

File tree

4 files changed

+213
-11
lines changed

4 files changed

+213
-11
lines changed

cmd/tuple/delete.go

+5-10
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@ package tuple
1919
import (
2020
"context"
2121
"fmt"
22-
"os"
2322

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

2826
"github.com/openfga/cli/internal/cmdutils"
2927
"github.com/openfga/cli/internal/output"
28+
"github.com/openfga/cli/internal/tuplefile"
3029
)
3130

3231
// deleteCmd represents the delete command.
@@ -46,19 +45,15 @@ var deleteCmd = &cobra.Command{
4645
if err != nil {
4746
return fmt.Errorf("failed to parse file name due to %w", err)
4847
}
48+
4949
if fileName != "" {
50-
var tuples []client.ClientTupleKeyWithoutCondition
5150

52-
data, err := os.ReadFile(fileName)
51+
clientTuples, err := tuplefile.ReadTupleFile(fileName)
5352
if err != nil {
5453
return fmt.Errorf("failed to read file %s due to %w", fileName, err)
5554
}
5655

57-
err = yaml.Unmarshal(data, &tuples)
58-
if err != nil {
59-
return fmt.Errorf("failed to parse input tuples due to %w", err)
60-
}
61-
56+
var openfgaTuples = tuplefile.ClientTupleKeyToTupleKeyWithoutCondition(clientTuples)
6257
maxTuplesPerWrite, err := cmd.Flags().GetInt("max-tuples-per-write")
6358
if err != nil {
6459
return fmt.Errorf("failed to parse max tuples per write due to %w", err)
@@ -70,7 +65,7 @@ var deleteCmd = &cobra.Command{
7065
}
7166

7267
deleteRequest := client.ClientWriteRequest{
73-
Deletes: tuples,
68+
Deletes: openfgaTuples,
7469
}
7570
response, err := ImportTuples(fgaClient, deleteRequest, maxTuplesPerWrite, maxParallelRequests)
7671
if err != nil {

cmd/tuple/delete_test.go

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package tuple
2+
3+
import (
4+
"testing"
5+
6+
openfga "github.com/openfga/go-sdk"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/openfga/cli/internal/tuplefile"
11+
)
12+
13+
func TestDeleteTuplesFileData(t *testing.T) { //nolint:funlen
14+
t.Parallel()
15+
16+
tests := []struct {
17+
name string
18+
file string
19+
expectedTuples []openfga.TupleKeyWithoutCondition
20+
expectedError string
21+
}{
22+
{
23+
name: "it can correctly parse a csv file",
24+
file: "testdata/tuples.csv",
25+
expectedTuples: []openfga.TupleKeyWithoutCondition{
26+
{
27+
User: "user:anne",
28+
Relation: "owner",
29+
Object: "folder:product",
30+
},
31+
{
32+
User: "folder:product",
33+
Relation: "parent",
34+
Object: "folder:product-2021",
35+
},
36+
{
37+
User: "team:fga#member",
38+
Relation: "viewer",
39+
Object: "folder:product-2021",
40+
},
41+
},
42+
},
43+
{
44+
name: "it can correctly parse a csv file regardless of columns order",
45+
file: "testdata/tuples_other_columns_order.csv",
46+
expectedTuples: []openfga.TupleKeyWithoutCondition{
47+
{
48+
User: "user:anne",
49+
Relation: "owner",
50+
Object: "folder:product",
51+
},
52+
{
53+
User: "folder:product",
54+
Relation: "parent",
55+
Object: "folder:product-2021",
56+
},
57+
{
58+
User: "team:fga#member",
59+
Relation: "viewer",
60+
Object: "folder:product-2021",
61+
},
62+
},
63+
},
64+
{
65+
name: "it can correctly parse a csv file without optional fields",
66+
file: "testdata/tuples_without_optional_fields.csv",
67+
expectedTuples: []openfga.TupleKeyWithoutCondition{
68+
{
69+
User: "user:anne",
70+
Relation: "owner",
71+
Object: "folder:product",
72+
},
73+
{
74+
User: "folder:product",
75+
Relation: "parent",
76+
Object: "folder:product-2021",
77+
},
78+
},
79+
},
80+
{
81+
name: "it can correctly parse a csv file with condition_name header but no condition_context header",
82+
file: "testdata/tuples_with_condition_name_but_no_condition_context.csv",
83+
expectedTuples: []openfga.TupleKeyWithoutCondition{
84+
{
85+
User: "user:anne",
86+
Relation: "owner",
87+
Object: "folder:product",
88+
},
89+
{
90+
User: "folder:product",
91+
Relation: "parent",
92+
Object: "folder:product-2021",
93+
},
94+
{
95+
User: "team:fga#member",
96+
Relation: "viewer",
97+
Object: "folder:product-2021",
98+
},
99+
},
100+
},
101+
{
102+
name: "it can correctly parse a json file",
103+
file: "testdata/tuples.json",
104+
expectedTuples: []openfga.TupleKeyWithoutCondition{
105+
{
106+
User: "user:anne",
107+
Relation: "owner",
108+
Object: "folder:product",
109+
},
110+
{
111+
User: "folder:product",
112+
Relation: "parent",
113+
Object: "folder:product-2021",
114+
},
115+
{
116+
User: "user:beth",
117+
Relation: "viewer",
118+
Object: "folder:product-2021",
119+
},
120+
},
121+
},
122+
{
123+
name: "it can correctly parse a yaml file",
124+
file: "testdata/tuples.yaml",
125+
expectedTuples: []openfga.TupleKeyWithoutCondition{
126+
{
127+
User: "user:anne",
128+
Relation: "owner",
129+
Object: "folder:product",
130+
},
131+
{
132+
User: "folder:product",
133+
Relation: "parent",
134+
Object: "folder:product-2021",
135+
},
136+
{
137+
User: "user:beth",
138+
Relation: "viewer",
139+
Object: "folder:product-2021",
140+
},
141+
},
142+
},
143+
{
144+
name: "it fails to parse a non-supported file format",
145+
file: "testdata/tuples.toml",
146+
expectedError: "failed to parse input tuples: unsupported file format \".toml\"",
147+
},
148+
{
149+
name: "it fails to parse a csv file with wrong headers",
150+
file: "testdata/tuples_wrong_headers.csv",
151+
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",
152+
},
153+
{
154+
name: "it fails to parse a csv file with missing required headers",
155+
file: "testdata/tuples_missing_required_headers.csv",
156+
expectedError: "failed to parse input tuples: csv header missing (\"object_id\")",
157+
},
158+
{
159+
name: "it fails to parse a csv file with missing condition_name header when condition_context is present",
160+
file: "testdata/tuples_missing_condition_name_header.csv",
161+
expectedError: "failed to parse input tuples: missing \"condition_name\" header which is required when \"condition_context\" is present",
162+
},
163+
{
164+
name: "it fails to parse an empty csv file",
165+
file: "testdata/tuples_empty.csv",
166+
expectedError: "failed to parse input tuples: failed to read csv headers: EOF",
167+
},
168+
{
169+
name: "it fails to parse a csv file with invalid rows",
170+
file: "testdata/tuples_with_invalid_rows.csv",
171+
expectedError: "failed to parse input tuples: failed to read tuple from csv file: record on line 2: wrong number of fields",
172+
},
173+
}
174+
175+
for _, test := range tests {
176+
test := test
177+
t.Run(test.name, func(t *testing.T) {
178+
t.Parallel()
179+
180+
actualTuples, err := tuplefile.ReadTupleFile(test.file)
181+
deleteTuples := tuplefile.ClientTupleKeyToTupleKeyWithoutCondition(actualTuples)
182+
183+
if test.expectedError != "" {
184+
require.EqualError(t, err, test.expectedError)
185+
186+
return
187+
}
188+
189+
require.NoError(t, err)
190+
assert.Equal(t, test.expectedTuples, deleteTuples)
191+
})
192+
}
193+
}

cmd/tuple/testdata/tuples.csv

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
user_type,user_id,user_relation,relation,object_type,object_id,condition_name,condition_context
22
user,anne,,owner,folder,product,inOfficeIP,
33
folder,product,,parent,folder,product-2021,inOfficeIP,"{""ip_addr"":""10.0.0.1""}"
4-
team,fga,member,viewer,folder,product-2021,,
4+
team,fga,member,viewer,folder,product-2021,,

internal/tuplefile/read.go

+14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"path"
77

8+
openfga "github.com/openfga/go-sdk"
89
"github.com/openfga/go-sdk/client"
910
"gopkg.in/yaml.v3"
1011
)
@@ -32,3 +33,16 @@ func ReadTupleFile(fileName string) ([]client.ClientTupleKey, error) {
3233

3334
return tuples, nil
3435
}
36+
37+
func ClientTupleKeyToTupleKeyWithoutCondition(clientTupleKey []client.ClientTupleKey) []openfga.TupleKeyWithoutCondition {
38+
var tuples []openfga.TupleKeyWithoutCondition
39+
for _, tuple := range clientTupleKey {
40+
convertedTuple := openfga.TupleKeyWithoutCondition{
41+
User: tuple.User,
42+
Relation: tuple.Relation,
43+
Object: tuple.Object,
44+
}
45+
tuples = append(tuples, convertedTuple)
46+
}
47+
return tuples
48+
}

0 commit comments

Comments
 (0)