Skip to content

Commit f3bd12d

Browse files
authored
feat: add JSONL tuple import support (#530)
* Add jsonl support for tuple write * Add condition to JSONL integration fixture * Update basic model and fixture files * fix wrapcheck warning * Update JSONL fixture user
1 parent 833d30d commit f3bd12d

File tree

10 files changed

+117
-4
lines changed

10 files changed

+117
-4
lines changed

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ fga tuple **write** <user> <relation> <object> --store-id=<store-id>
708708
* `--condition-context`: Condition context (optional)
709709
* `--store-id`: Specifies the store id
710710
* `--model-id`: Specifies the model id to target (optional)
711-
* `--file`: Specifies the file name, `json`, `yaml` and `csv` files are supported
711+
* `--file`: Specifies the file name, `json`, `jsonl`, `yaml` and `csv` files are supported
712712
* `--max-tuples-per-write`: Max tuples to send in a single write (optional, default=1, or 40 if `--max-rps` is set and this flag is omitted)
713713
* `--max-parallel-requests`: Max requests to send in parallel (optional, default=4, or `max-rps/5` if `--max-rps` is set and this flag is omitted)
714714
* `--hide-imported-tuples`: When importing from a file, do not output successfully imported tuples in the command output (optional, default=false)
@@ -757,6 +757,14 @@ If using a `yaml` file, the format should be:
757757
object: folder:product-2021Q1
758758
```
759759

760+
If using a `jsonl` file, the format should be:
761+
762+
```jsonl
763+
{"user": "user:anne", "relation": "owner", "object": "folder:product"}
764+
{"user": "folder:product", "relation": "parent", "object": "folder:product-2021", "condition": {"name": "inOfficeIP", "context": {"ip_addr": "10.0.0.1"}}}
765+
{"user": "user:beth", "relation": "viewer", "object": "folder:product-2021"}
766+
```
767+
760768
If using a `json` file, the format should be:
761769

762770
```json
@@ -845,7 +853,7 @@ fga tuple **delete** <user> <relation> <object> --store-id=<store-id>
845853
* `<object>`: Object
846854
* `--store-id`: Specifies the store id
847855
* `--model-id`: Specifies the model id to target (optional)
848-
* `--file`: Specifies the file name, `yaml` and `json` files are supported
856+
* `--file`: Specifies the file name, `yaml`, `json`, and `jsonl` files are supported
849857
* `--max-tuples-per-write`: Max tuples to send in a single write (optional, default=1)
850858
* `--max-parallel-requests`: Max requests to send in parallel (optional, default=4)
851859

cmd/tuple/delete_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,27 @@ func TestDeleteTuplesFileData(t *testing.T) {
120120
},
121121
},
122122
},
123+
{
124+
name: "it can correctly parse a jsonl file",
125+
file: "testdata/tuples.jsonl",
126+
expectedTuples: []openfga.TupleKeyWithoutCondition{
127+
{
128+
User: "user:anne",
129+
Relation: "owner",
130+
Object: "folder:product",
131+
},
132+
{
133+
User: "folder:product",
134+
Relation: "parent",
135+
Object: "folder:product-2021",
136+
},
137+
{
138+
User: "user:beth",
139+
Relation: "viewer",
140+
Object: "folder:product-2021",
141+
},
142+
},
143+
},
123144
{
124145
name: "it can correctly parse a yaml file",
125146
file: "testdata/tuples.yaml",
@@ -179,6 +200,11 @@ func TestDeleteTuplesFileData(t *testing.T) {
179200
file: "testdata/tuples_empty.json",
180201
expectedError: "failed to parse input tuples: tuples file is empty (json)",
181202
},
203+
{
204+
name: "empty jsonl file should throw a warning",
205+
file: "testdata/tuples_empty.jsonl",
206+
expectedError: "failed to parse input tuples: tuples file is empty (jsonl)",
207+
},
182208
{
183209
name: "empty yaml file should throw a warning",
184210
file: "testdata/tuples_empty.yaml",

cmd/tuple/testdata/tuples.jsonl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{"user":"user:anne","relation":"owner","object":"folder:product"}
2+
{"user":"folder:product","relation":"parent","object":"folder:product-2021","condition":{"name":"inOfficeIP","context":{"ip_addr":"10.0.0.1"}}}
3+
{"user":"user:beth","relation":"viewer","object":"folder:product-2021"}

cmd/tuple/testdata/tuples_empty.jsonl

Whitespace-only changes.

cmd/tuple/write_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,31 @@ func TestParseTuplesFileData(t *testing.T) {
148148
},
149149
},
150150
},
151+
{
152+
name: "it can correctly parse a jsonl file",
153+
file: "testdata/tuples.jsonl",
154+
expectedTuples: []client.ClientTupleKey{
155+
{
156+
User: "user:anne",
157+
Relation: "owner",
158+
Object: "folder:product",
159+
},
160+
{
161+
User: "folder:product",
162+
Relation: "parent",
163+
Object: "folder:product-2021",
164+
Condition: &openfga.RelationshipCondition{
165+
Name: "inOfficeIP",
166+
Context: &map[string]interface{}{"ip_addr": "10.0.0.1"},
167+
},
168+
},
169+
{
170+
User: "user:beth",
171+
Relation: "viewer",
172+
Object: "folder:product-2021",
173+
},
174+
},
175+
},
151176
{
152177
name: "it can correctly parse a yaml file",
153178
file: "testdata/tuples.yaml",

internal/tuplefile/read.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package tuplefile
22

33
import (
4+
"bufio"
5+
"bytes"
6+
"encoding/json"
47
"fmt"
58
"os"
69
"path"
@@ -26,6 +29,8 @@ func ReadTupleFile(fileName string) ([]client.ClientTupleKey, error) {
2629
if err == nil && len(tuples) == 0 {
2730
err = clierrors.EmptyTuplesFileError(strings.TrimPrefix(path.Ext(fileName), "."))
2831
}
32+
case ".jsonl":
33+
err = parseTuplesFromJSONL(data, &tuples)
2934
case ".csv":
3035
err = parseTuplesFromCSV(data, &tuples)
3136
default:
@@ -38,3 +43,37 @@ func ReadTupleFile(fileName string) ([]client.ClientTupleKey, error) {
3843

3944
return tuples, nil
4045
}
46+
47+
func parseTuplesFromJSONL(data []byte, tuples *[]client.ClientTupleKey) error {
48+
scanner := bufio.NewScanner(bytes.NewReader(data))
49+
lineNum := 0
50+
51+
for scanner.Scan() {
52+
lineNum++
53+
54+
line := strings.TrimSpace(scanner.Text())
55+
if line == "" {
56+
continue
57+
}
58+
59+
var tuple client.ClientTupleKey
60+
61+
err := json.Unmarshal([]byte(line), &tuple)
62+
if err != nil {
63+
return fmt.Errorf("failed to read tuple from jsonl file on line %d: %w", lineNum, err)
64+
}
65+
66+
*tuples = append(*tuples, tuple)
67+
}
68+
69+
err := scanner.Err()
70+
if err != nil {
71+
return fmt.Errorf("failed to read jsonl file: %w", err)
72+
}
73+
74+
if len(*tuples) == 0 {
75+
return clierrors.EmptyTuplesFileError("jsonl") //nolint:wrapcheck
76+
}
77+
78+
return nil
79+
}

tests/fixtures/basic-model.fga

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ type user
55

66
type group
77
relations
8-
define owner: [user]
8+
define owner: [user, user with inOfficeIP]
9+
10+
condition inOfficeIP(ip_addr: ipaddress) {
11+
ip_addr.in_cidr("192.168.0.0/24")
12+
}

tests/fixtures/basic-tuples.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
"relation": "owner",
55
"object": "group:foo"
66
}
7-
]
7+
]

tests/fixtures/basic-tuples.jsonl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{"user": "user:bob", "relation": "owner", "object": "group:foo", "condition": {"name": "inOfficeIP", "context": {"ip_addr": "10.0.0.1"}}}
2+

tests/tuple-test-cases.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ tests:
88
stdout:
99
json:
1010
successful.0.user: "user:anne"
11+
001b - it successfully writes tuples to a store using jsonl:
12+
command: fga tuple write --file=./tests/fixtures/basic-tuples.jsonl --max-tuples-per-write=1 --max-parallel-requests=1 --store-id=$(./tests/scripts/test-data/get-store-id.sh) --model-id=$(./tests/scripts/test-data/get-model-id.sh)
13+
exit-code: 0
14+
stdout:
15+
json:
16+
successful.0.user: "user:bob"
1117
002 - it successfully deletes tuples from a store:
1218
command: fga tuple delete --file=./tests/fixtures/basic-tuples.json --max-tuples-per-write=1 --max-parallel-requests=1 --store-id=$(./tests/scripts/test-data/get-store-id.sh) --model-id=$(./tests/scripts/test-data/get-model-id.sh)
1319
exit-code: 0

0 commit comments

Comments
 (0)