Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ jobs:

- name: Run SDK Tests
run: make test-client-go
env:
OPEN_API_REF: 0ac19aac54f21f3c78970126b84b4c69c6e3b9a2

- name: Check for SDK changes
run: |
Expand Down
3 changes: 3 additions & 0 deletions config/clients/go/CHANGELOG.md.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v{{packageVersion}}...HEAD)

- **Conflict options for Write operations**: (#229)
The client now supports setting `Conflict` on `ClientWriteOptions` to control behavior when writing duplicate tuples or deleting non-existent tuples. This feature requires OpenFGA server [v1.10.0](https://github.com/openfga/openfga/releases/tag/v1.10.0) or later.
See [Conflict Options for Write Operations](./README.md#conflict-options-for-write-operations) for more.
- feat: add contextual tuples support in Expand requests

## v0.7.1
Expand Down
4 changes: 4 additions & 0 deletions config/clients/go/config.overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@
"destinationFilename": "internal/utils/ulid_test.go",
"templateType": "SupportingFiles"
},
"models_test.mustache": {
"destinationFilename": "models_test.go",
"templateType": "SupportingFiles"
},
"example/Makefile": {},
"example/README.md": {},
"example/example1/example1.go": {},
Expand Down
36 changes: 36 additions & 0 deletions config/clients/go/template/README_calling_api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,42 @@ data, err := fgaClient.Write(context.Background()).Body(body).Options(options).E
// }]
```

#### Conflict Options for Write Operations

The SDK supports conflict options for write operations, allowing you to control how the API handles duplicate writes and missing deletes.

> Note: This requires OpenFGA [v1.10.0](https://github.com/openfga/openfga/releases/tag/v1.10.0) or later.

```go
options := ClientWriteOptions{
Conflict: ClientWriteConflictOptions{
// Control what happens when writing a tuple that already exists
WritesOnDuplicate: CLIENT_WRITE_REQUEST_WRITES_ON_DUPLICATE_IGNORE, // or CLIENT_WRITE_REQUEST_WRITES_ON_DUPLICATE_ERROR (the current default behavior)

// Control what happens when deleting a tuple that doesn't exist
DeleteOnMissing: CLIENT_WRITE_REQUEST_DELETES_ON_MISSING_IGNORE, // or CLIENT_WRITE_REQUEST_DELETES_ON_MISSING_ERROR (the current default behavior)
},
}

body := ClientWriteRequest{
Writes: []ClientTupleKey{ {
User: "user:anne",
Relation: "writer",
Object: "document:2021-budget",
} },
Deletes: []ClientTupleKeyWithoutCondition{ {
User: "user:bob",
Relation: "reader",
Object: "document:2021-budget",
} },
}

data, err := fgaClient.Write(context.Background()).
Body(body).
Options(options).
Execute()
```

#### Relationship Queries

##### Check
Expand Down
302 changes: 302 additions & 0 deletions config/clients/go/template/api_test.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,308 @@ func Test{{appShortName}}Api(t *testing.T) {
}
})

t.Run("Write (Write Tuple with OnDuplicate ignore)", func(t *testing.T) {
test := TestDefinition{
Name: "Write",
JsonResponse: `{}`,
ResponseStatus: 200,
Method: "POST",
RequestPath: "write",
}
onDuplicateIgnore := "ignore"
{{=<% %>=}}
requestBody := WriteRequest{
Writes: &WriteRequestWrites{
TupleKeys: []TupleKey{{
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
Relation: "viewer",
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
}},
OnDuplicate: &onDuplicateIgnore,
},
AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"),
}
<%={{ }}=%>

var expectedResponse map[string]interface{}
if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil {
t.Fatalf("%v", err)
}

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath),
func(req *http.Request) (*http.Response, error) {
// Verify the request body contains the OnDuplicate field
var body WriteRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
t.Errorf("Failed to decode request body: %v", err)
}
if body.Writes.OnDuplicate == nil || *body.Writes.OnDuplicate != "ignore" {
t.Errorf("Expected OnDuplicate to be 'ignore', got %v", body.Writes.OnDuplicate)
}

resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse)
if err != nil {
return httpmock.NewStringResponse(500, ""), nil
}
return resp, nil
},
)
_, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute()
if err != nil {
t.Fatalf("%v", err)
}

if response.StatusCode != test.ResponseStatus {
t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus)
}
})

t.Run("Write (Write Tuple with OnDuplicate error)", func(t *testing.T) {
test := TestDefinition{
Name: "Write",
JsonResponse: `{}`,
ResponseStatus: 200,
Method: "POST",
RequestPath: "write",
}
onDuplicateError := "error"
{{=<% %>=}}
requestBody := WriteRequest{
Writes: &WriteRequestWrites{
TupleKeys: []TupleKey{{
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
Relation: "viewer",
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
}},
OnDuplicate: &onDuplicateError,
},
AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"),
}
<%={{ }}=%>

var expectedResponse map[string]interface{}
if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil {
t.Fatalf("%v", err)
}

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath),
func(req *http.Request) (*http.Response, error) {
// Verify the request body contains the OnDuplicate field
var body WriteRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
t.Errorf("Failed to decode request body: %v", err)
}
if body.Writes.OnDuplicate == nil || *body.Writes.OnDuplicate != "error" {
t.Errorf("Expected OnDuplicate to be 'error', got %v", body.Writes.OnDuplicate)
}

resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse)
if err != nil {
return httpmock.NewStringResponse(500, ""), nil
}
return resp, nil
},
)
_, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute()
if err != nil {
t.Fatalf("%v", err)
}

if response.StatusCode != test.ResponseStatus {
t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus)
}
})

t.Run("Write (Delete Tuple with OnMissing ignore)", func(t *testing.T) {
test := TestDefinition{
Name: "Write",
JsonResponse: `{}`,
ResponseStatus: 200,
Method: "POST",
RequestPath: "write",
}
onMissingIgnore := "ignore"
{{=<% %>=}}
requestBody := WriteRequest{
Deletes: &WriteRequestDeletes{
TupleKeys: []TupleKeyWithoutCondition{{
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
Relation: "viewer",
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
}},
OnMissing: &onMissingIgnore,
},
AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"),
}
<%={{ }}=%>

var expectedResponse map[string]interface{}
if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil {
t.Fatalf("%v", err)
}

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath),
func(req *http.Request) (*http.Response, error) {
// Verify the request body contains the OnMissing field
var body WriteRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
t.Errorf("Failed to decode request body: %v", err)
}
if body.Deletes.OnMissing == nil || *body.Deletes.OnMissing != "ignore" {
t.Errorf("Expected OnMissing to be 'ignore', got %v", body.Deletes.OnMissing)
}

resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse)
if err != nil {
return httpmock.NewStringResponse(500, ""), nil
}
return resp, nil
},
)
_, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute()
if err != nil {
t.Fatalf("%v", err)
}

if response.StatusCode != test.ResponseStatus {
t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus)
}
})

t.Run("Write (Delete Tuple with OnMissing error)", func(t *testing.T) {
test := TestDefinition{
Name: "Write",
JsonResponse: `{}`,
ResponseStatus: 200,
Method: "POST",
RequestPath: "write",
}
onMissingError := "error"
{{=<% %>=}}
requestBody := WriteRequest{
Deletes: &WriteRequestDeletes{
TupleKeys: []TupleKeyWithoutCondition{{
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
Relation: "viewer",
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
}},
OnMissing: &onMissingError,
},
AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"),
}
<%={{ }}=%>

var expectedResponse map[string]interface{}
if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil {
t.Fatalf("%v", err)
}

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath),
func(req *http.Request) (*http.Response, error) {
// Verify the request body contains the OnMissing field
var body WriteRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
t.Errorf("Failed to decode request body: %v", err)
}
if body.Deletes.OnMissing == nil || *body.Deletes.OnMissing != "error" {
t.Errorf("Expected OnMissing to be 'error', got %v", body.Deletes.OnMissing)
}

resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse)
if err != nil {
return httpmock.NewStringResponse(500, ""), nil
}
return resp, nil
},
)
_, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute()
if err != nil {
t.Fatalf("%v", err)
}

if response.StatusCode != test.ResponseStatus {
t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus)
}
})

t.Run("Write (Mixed writes and deletes with conflict options)", func(t *testing.T) {
test := TestDefinition{
Name: "Write",
JsonResponse: `{}`,
ResponseStatus: 200,
Method: "POST",
RequestPath: "write",
}
onDuplicateIgnore := "ignore"
onMissingIgnore := "ignore"
{{=<% %>=}}
requestBody := WriteRequest{
Writes: &WriteRequestWrites{
TupleKeys: []TupleKey{{
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
Relation: "viewer",
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
}},
OnDuplicate: &onDuplicateIgnore,
},
Deletes: &WriteRequestDeletes{
TupleKeys: []TupleKeyWithoutCondition{{
User: "user:another-user",
Relation: "viewer",
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
}},
OnMissing: &onMissingIgnore,
},
AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"),
}
<%={{ }}=%>

var expectedResponse map[string]interface{}
if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil {
t.Fatalf("%v", err)
}

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath),
func(req *http.Request) (*http.Response, error) {
// Verify the request body contains both OnDuplicate and OnMissing fields
var body WriteRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
t.Errorf("Failed to decode request body: %v", err)
}
if body.Writes.OnDuplicate == nil || *body.Writes.OnDuplicate != "ignore" {
t.Errorf("Expected OnDuplicate to be 'ignore', got %v", body.Writes.OnDuplicate)
}
if body.Deletes.OnMissing == nil || *body.Deletes.OnMissing != "ignore" {
t.Errorf("Expected OnMissing to be 'ignore', got %v", body.Deletes.OnMissing)
}

resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse)
if err != nil {
return httpmock.NewStringResponse(500, ""), nil
}
return resp, nil
},
)
_, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute()
if err != nil {
t.Fatalf("%v", err)
}

if response.StatusCode != test.ResponseStatus {
t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus)
}
})

t.Run("Expand", func(t *testing.T) {
test := TestDefinition{
Name: "Expand",
Expand Down
Loading
Loading