Skip to content
Merged
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
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 @@ -3,6 +3,9 @@
## [Unreleased](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v{{packageVersion}}...HEAD)

- feat: add support for custom headers per request. See [documentation](https://github.com/openfga/go-sdk#custom-headers).
- feat: add support for 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.

## {{packageVersion}}

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 @@ -104,6 +104,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 @@ -409,6 +409,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
OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, // or CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_ERROR (the current default behavior)

// Control what happens when deleting a tuple that doesn't exist
OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, // or CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_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 @@ -789,6 +789,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