diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 7505b6e..32e6240 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -212,6 +212,7 @@ model_write_authorization_model_response.go model_write_request.go model_write_request_deletes.go model_write_request_writes.go +models_test.go oauth2/LICENSE oauth2/ORIGINAL_AUTHORS oauth2/ORIGINAL_CONTRIBUTORS diff --git a/CHANGELOG.md b/CHANGELOG.md index eb24414..f21f8e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## [Unreleased](https://github.com/openfga/go-sdk/compare/v0.7.2...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. ## 0.7.2 diff --git a/README.md b/README.md index 62198e4..cef369b 100644 --- a/README.md +++ b/README.md @@ -680,6 +680,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 diff --git a/api_open_fga.go b/api_open_fga.go index f32eb4c..309b182 100644 --- a/api_open_fga.go +++ b/api_open_fga.go @@ -775,7 +775,10 @@ type OpenFgaApi interface { * Write Add or delete tuples from the store * The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. - The API is not idempotent: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. + The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. + To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. + To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. + If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example @@ -790,7 +793,8 @@ type OpenFgaApi interface { "relation": "writer", "object": "document:2021-budget" } - ] + ], + "on_duplicate": "ignore" }, "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" } @@ -806,7 +810,8 @@ type OpenFgaApi interface { "relation": "reader", "object": "document:2021-budget" } - ] + ], + "on_missing": "ignore" } } ``` @@ -4192,7 +4197,10 @@ func (r ApiWriteRequest) Execute() (map[string]interface{}, *http.Response, erro - The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. -The API is not idempotent: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. +The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. +To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. +To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. +If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example @@ -4208,7 +4216,8 @@ To add `user:anne` as a `writer` for `document:2021-budget`, call write API with "relation": "writer", "object": "document:2021-budget" } - ] + ], + "on_duplicate": "ignore" }, "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" } @@ -4226,7 +4235,8 @@ To remove `user:bob` as a `reader` for `document:2021-budget`, call write API wi "relation": "reader", "object": "document:2021-budget" } - ] + ], + "on_missing": "ignore" } } diff --git a/api_open_fga_test.go b/api_open_fga_test.go index fcff26d..b83b815 100644 --- a/api_open_fga_test.go +++ b/api_open_fga_test.go @@ -794,6 +794,298 @@ func TestOpenFgaApi(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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus) + } + }) + t.Run("Expand", func(t *testing.T) { test := TestDefinition{ Name: "Expand", diff --git a/client/client.go b/client/client.go index 6248573..d9f6355 100644 --- a/client/client.go +++ b/client/client.go @@ -1515,12 +1515,62 @@ type TransactionOptions struct { MaxParallelRequests int32 `json:"max_parallel_requests,omitempty"` } +// ClientWriteRequestOnDuplicateWrites indicates what to do when a write conflicts with an existing tuple +type ClientWriteRequestOnDuplicateWrites string + +func (w *ClientWriteRequestOnDuplicateWrites) ToString() *string { + if w == nil { + return nil + } + + str := string(*w) + + return &str +} + +const ( + // CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_ERROR returns an error if a write conflicts with an existing tuple (default) + CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_ERROR ClientWriteRequestOnDuplicateWrites = "error" + // CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE ignores writes that conflict with existing tuples (they must match exactly, including conditions) + CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE ClientWriteRequestOnDuplicateWrites = "ignore" +) + +// ClientWriteRequestOnMissingDeletes indicates what to do when a delete is issued for a tuple that does not exist +type ClientWriteRequestOnMissingDeletes string + +func (d *ClientWriteRequestOnMissingDeletes) ToString() *string { + if d == nil { + return nil + } + + str := string(*d) + + return &str +} + +const ( + // CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_ERROR returns an error if a delete is issued for a tuple that does not exist (default) + CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_ERROR ClientWriteRequestOnMissingDeletes = "error" + // CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE ignores deletes for tuples that do not exist + CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE ClientWriteRequestOnMissingDeletes = "ignore" +) + +type ClientWriteConflictOptions struct { + // OnDuplicateWrites defines what to do when a write conflicts with an existing tuple + // Options are: "error" (default) or "ignore" + OnDuplicateWrites ClientWriteRequestOnDuplicateWrites `json:"on_duplicate_writes,omitempty"` + // OnMissingDeletes defines what to do when a delete is issued for a tuple that does not exist + // Options are: "error" (default) or "ignore" + OnMissingDeletes ClientWriteRequestOnMissingDeletes `json:"on_missing_deletes,omitempty"` +} + type ClientWriteOptions struct { RequestOptions AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` Transaction *TransactionOptions `json:"transaction_options,omitempty"` + Conflict ClientWriteConflictOptions } type ClientWriteStatus string @@ -1658,6 +1708,9 @@ func (client *OpenFgaClient) WriteExecute(request SdkClientWriteRequestInterface } if len(request.GetBody().Writes) > 0 { writes := fgaSdk.WriteRequestWrites{} + if options != nil { + writes.OnDuplicate = options.Conflict.OnDuplicateWrites.ToString() + } for index := 0; index < len(request.GetBody().Writes); index++ { writes.TupleKeys = append(writes.TupleKeys, (request.GetBody().Writes)[index]) } @@ -1665,6 +1718,9 @@ func (client *OpenFgaClient) WriteExecute(request SdkClientWriteRequestInterface } if len(request.GetBody().Deletes) > 0 { deletes := fgaSdk.WriteRequestDeletes{} + if options != nil { + deletes.OnMissing = options.Conflict.OnMissingDeletes.ToString() + } for index := 0; index < len(request.GetBody().Deletes); index++ { deletes.TupleKeys = append(deletes.TupleKeys, (request.GetBody().Deletes)[index]) } @@ -1750,6 +1806,7 @@ func (client *OpenFgaClient) WriteExecute(request SdkClientWriteRequestInterface RequestOptions: options.RequestOptions, AuthorizationModelId: authorizationModelId, StoreId: request.GetStoreIdOverride(), + Conflict: options.Conflict, }, }) @@ -1795,6 +1852,7 @@ func (client *OpenFgaClient) WriteExecute(request SdkClientWriteRequestInterface RequestOptions: options.RequestOptions, AuthorizationModelId: authorizationModelId, StoreId: request.GetStoreIdOverride(), + Conflict: options.Conflict, }, }) diff --git a/client/client_test.go b/client/client_test.go index 683206e..f051e50 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -3688,6 +3688,665 @@ func TestOpenFgaClient(t *testing.T) { }) } +func TestOpenFgaClientWriteClientWriteConflictOptions(t *testing.T) { + fgaClient, err := NewSdkClient(&ClientConfiguration{ + ApiUrl: "https://api.fga.example", + StoreId: "01GXSB9YR785C4FYS3C0RTG7B2", + }) + if err != nil { + t.Fatalf("%v", err) + } + + t.Run("Client Write with OnDuplicateWrites ignore option", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + } + + 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", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnDuplicate field set to "ignore" + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", requestBody.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with OnDuplicateWrites error option", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + } + + 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", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnDuplicate field set to "error" + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "error" { + t.Errorf("Expected OnDuplicate to be 'error', got %v", requestBody.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_ERROR, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with OnMissingDeletes ignore option", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Deletes: []ClientTupleKeyWithoutCondition{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + } + + 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", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnMissing field set to "ignore" + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Deletes == nil || requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "ignore" { + t.Errorf("Expected OnMissing to be 'ignore', got %v", requestBody.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with OnMissingDeletes error option", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Deletes: []ClientTupleKeyWithoutCondition{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + } + + 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", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnMissing field set to "error" + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Deletes == nil || requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "error" { + t.Errorf("Expected OnMissing to be 'error', got %v", requestBody.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_ERROR, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with both OnDuplicateWrites and OnMissingDeletes options", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + Deletes: []ClientTupleKeyWithoutCondition{{ + User: "user:another-user", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + } + + 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", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains both OnDuplicate and OnMissing fields + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", requestBody.Writes.OnDuplicate) + } + if requestBody.Deletes == nil || requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "ignore" { + t.Errorf("Expected OnMissing to be 'ignore', got %v", requestBody.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with conflict options and transaction disabled", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + { + User: "user:another-user", + Relation: "viewer", + Object: "document:another-doc", + }, + }, + } + + 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", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify each chunked request contains the OnDuplicate field + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore' in chunked request, got %v", requestBody.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, + Transaction: &TransactionOptions{ + Disable: true, + MaxPerChunk: 1, + MaxParallelRequests: 2, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with conflict options and authorization model", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + modelId := "01GAHCE4YVKPQEKZQHT2R89MQV" + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request contains both conflict options and authorization model + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", requestBody.Writes.OnDuplicate) + } + if requestBody.AuthorizationModelId == nil || *requestBody.AuthorizationModelId != modelId { + t.Errorf("Expected AuthorizationModelId to be %s, got %v", modelId, requestBody.AuthorizationModelId) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, + AuthorizationModelId: &modelId, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with store override and conflict options", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + overrideStoreId := "01GXSB9YR785C4FYS3C0RTG7B3" + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, overrideStoreId, test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request contains conflict options + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", requestBody.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, + StoreId: &overrideStoreId, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write conflict options precedence test", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + { + User: "user:another-user", + Relation: "editor", + Object: "document:another-doc", + }, + }, + Deletes: []ClientTupleKeyWithoutCondition{ + { + User: "user:old-user", + Relation: "viewer", + Object: "document:old-doc", + }, + }, + } + + 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", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify that client options override any default values + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected client OnDuplicateWrites option to override defaults, got %v", requestBody.Writes.OnDuplicate) + } + + if requestBody.Deletes == nil || requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "ignore" { + t.Errorf("Expected client OnMissingDeletes option to override defaults, got %v", requestBody.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, + }, + } + + result, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if result == nil { + t.Fatalf("Expected non-nil result") + } + + if len(result.Writes) != 2 { + t.Fatalf("Expected 2 write results, got %d", len(result.Writes)) + } + + if len(result.Deletes) != 1 { + t.Fatalf("Expected 1 delete result, got %d", len(result.Deletes)) + } + }) + + t.Run("Client Write mixed conflict options with chunked requests", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ + { + User: "user:writer1", + Relation: "viewer", + Object: "document:doc1", + }, + { + User: "user:writer2", + Relation: "viewer", + Object: "document:doc2", + }, + }, + Deletes: []ClientTupleKeyWithoutCondition{ + { + User: "user:deleter1", + Relation: "viewer", + Object: "document:doc3", + }, + { + User: "user:deleter2", + Relation: "viewer", + Object: "document:doc4", + }, + }, + } + + 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", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + var requestBody openfga.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + + if requestBody.Writes != nil && len(requestBody.Writes.TupleKeys) > 0 { + if requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore' in write chunk, got %v", requestBody.Writes.OnDuplicate) + } + } + + if requestBody.Deletes != nil && len(requestBody.Deletes.TupleKeys) > 0 { + if requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "ignore" { + t.Errorf("Expected OnMissing to be 'ignore' in delete chunk, got %v", requestBody.Deletes.OnMissing) + } + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, + }, + Transaction: &TransactionOptions{ + Disable: true, + MaxPerChunk: 1, + MaxParallelRequests: 4, + }, + } + + result, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if result == nil { + t.Fatalf("Expected non-nil result") + } + + if len(result.Writes) != 2 { + t.Fatalf("Expected 2 write results, got %d", len(result.Writes)) + } + + if len(result.Deletes) != 2 { + t.Fatalf("Expected 2 delete results, got %d", len(result.Deletes)) + } + }) +} + func getStoreId(t *testing.T, fgaClient *OpenFgaClient) string { storeId, err := fgaClient.GetStoreId() if err != nil { diff --git a/docs/WriteRequestDeletes.md b/docs/WriteRequestDeletes.md index 9ef9418..03ef213 100644 --- a/docs/WriteRequestDeletes.md +++ b/docs/WriteRequestDeletes.md @@ -5,6 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **TupleKeys** | [**[]TupleKeyWithoutCondition**](TupleKeyWithoutCondition.md) | | +**OnMissing** | Pointer to **string** | On 'error', the API returns an error when deleting a tuple that does not exist. On 'ignore', deletes of non-existent tuples are treated as no-ops. | [optional] [default to "error"] ## Methods @@ -45,6 +46,31 @@ and a boolean to check if the value has been set. SetTupleKeys sets TupleKeys field to given value. +### GetOnMissing + +`func (o *WriteRequestDeletes) GetOnMissing() string` + +GetOnMissing returns the OnMissing field if non-nil, zero value otherwise. + +### GetOnMissingOk + +`func (o *WriteRequestDeletes) GetOnMissingOk() (*string, bool)` + +GetOnMissingOk returns a tuple with the OnMissing field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetOnMissing + +`func (o *WriteRequestDeletes) SetOnMissing(v string)` + +SetOnMissing sets OnMissing field to given value. + +### HasOnMissing + +`func (o *WriteRequestDeletes) HasOnMissing() bool` + +HasOnMissing returns a boolean if a field has been set. + [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/docs/WriteRequestWrites.md b/docs/WriteRequestWrites.md index 0f8aa86..04d1483 100644 --- a/docs/WriteRequestWrites.md +++ b/docs/WriteRequestWrites.md @@ -5,6 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **TupleKeys** | [**[]TupleKey**](TupleKey.md) | | +**OnDuplicate** | Pointer to **string** | On 'error' ( or unspecified ), the API returns an error if an identical tuple already exists. On 'ignore', identical writes are treated as no-ops (matching on user, relation, object, and RelationshipCondition). | [optional] [default to "error"] ## Methods @@ -45,6 +46,31 @@ and a boolean to check if the value has been set. SetTupleKeys sets TupleKeys field to given value. +### GetOnDuplicate + +`func (o *WriteRequestWrites) GetOnDuplicate() string` + +GetOnDuplicate returns the OnDuplicate field if non-nil, zero value otherwise. + +### GetOnDuplicateOk + +`func (o *WriteRequestWrites) GetOnDuplicateOk() (*string, bool)` + +GetOnDuplicateOk returns a tuple with the OnDuplicate field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetOnDuplicate + +`func (o *WriteRequestWrites) SetOnDuplicate(v string)` + +SetOnDuplicate sets OnDuplicate field to given value. + +### HasOnDuplicate + +`func (o *WriteRequestWrites) HasOnDuplicate() bool` + +HasOnDuplicate returns a boolean if a field has been set. + [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/example/example1/example1.go b/example/example1/example1.go index 3658664..d7853c7 100644 --- a/example/example1/example1.go +++ b/example/example1/example1.go @@ -200,6 +200,10 @@ func mainInner() error { }, }).Options(client.ClientWriteOptions{ AuthorizationModelId: &authorizationModel.AuthorizationModelId, + Conflict: client.ClientWriteConflictOptions{ + // We can choose to ignore conflicts during writes + OnDuplicateWrites: client.CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, }).Execute() if err != nil { return err diff --git a/model_write_request_deletes.go b/model_write_request_deletes.go index 0667968..a1aac13 100644 --- a/model_write_request_deletes.go +++ b/model_write_request_deletes.go @@ -21,6 +21,8 @@ import ( // WriteRequestDeletes struct for WriteRequestDeletes type WriteRequestDeletes struct { TupleKeys []TupleKeyWithoutCondition `json:"tuple_keys" yaml:"tuple_keys"` + // On 'error', the API returns an error when deleting a tuple that does not exist. On 'ignore', deletes of non-existent tuples are treated as no-ops. + OnMissing *string `json:"on_missing,omitempty" yaml:"on_missing,omitempty"` } // NewWriteRequestDeletes instantiates a new WriteRequestDeletes object @@ -30,6 +32,8 @@ type WriteRequestDeletes struct { func NewWriteRequestDeletes(tupleKeys []TupleKeyWithoutCondition) *WriteRequestDeletes { this := WriteRequestDeletes{} this.TupleKeys = tupleKeys + var onMissing = "error" + this.OnMissing = &onMissing return &this } @@ -38,6 +42,8 @@ func NewWriteRequestDeletes(tupleKeys []TupleKeyWithoutCondition) *WriteRequestD // but it doesn't guarantee that properties required by API are set func NewWriteRequestDeletesWithDefaults() *WriteRequestDeletes { this := WriteRequestDeletes{} + var onMissing = "error" + this.OnMissing = &onMissing return &this } @@ -65,9 +71,44 @@ func (o *WriteRequestDeletes) SetTupleKeys(v []TupleKeyWithoutCondition) { o.TupleKeys = v } +// GetOnMissing returns the OnMissing field value if set, zero value otherwise. +func (o *WriteRequestDeletes) GetOnMissing() string { + if o == nil || o.OnMissing == nil { + var ret string + return ret + } + return *o.OnMissing +} + +// GetOnMissingOk returns a tuple with the OnMissing field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *WriteRequestDeletes) GetOnMissingOk() (*string, bool) { + if o == nil || o.OnMissing == nil { + return nil, false + } + return o.OnMissing, true +} + +// HasOnMissing returns a boolean if a field has been set. +func (o *WriteRequestDeletes) HasOnMissing() bool { + if o != nil && o.OnMissing != nil { + return true + } + + return false +} + +// SetOnMissing gets a reference to the given string and assigns it to the OnMissing field. +func (o *WriteRequestDeletes) SetOnMissing(v string) { + o.OnMissing = &v +} + func (o WriteRequestDeletes) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} toSerialize["tuple_keys"] = o.TupleKeys + if o.OnMissing != nil { + toSerialize["on_missing"] = o.OnMissing + } var b bytes.Buffer enc := json.NewEncoder(&b) enc.SetEscapeHTML(false) diff --git a/model_write_request_writes.go b/model_write_request_writes.go index 18b7d5b..af3b002 100644 --- a/model_write_request_writes.go +++ b/model_write_request_writes.go @@ -21,6 +21,8 @@ import ( // WriteRequestWrites struct for WriteRequestWrites type WriteRequestWrites struct { TupleKeys []TupleKey `json:"tuple_keys" yaml:"tuple_keys"` + // On 'error' ( or unspecified ), the API returns an error if an identical tuple already exists. On 'ignore', identical writes are treated as no-ops (matching on user, relation, object, and RelationshipCondition). + OnDuplicate *string `json:"on_duplicate,omitempty" yaml:"on_duplicate,omitempty"` } // NewWriteRequestWrites instantiates a new WriteRequestWrites object @@ -30,6 +32,8 @@ type WriteRequestWrites struct { func NewWriteRequestWrites(tupleKeys []TupleKey) *WriteRequestWrites { this := WriteRequestWrites{} this.TupleKeys = tupleKeys + var onDuplicate = "error" + this.OnDuplicate = &onDuplicate return &this } @@ -38,6 +42,8 @@ func NewWriteRequestWrites(tupleKeys []TupleKey) *WriteRequestWrites { // but it doesn't guarantee that properties required by API are set func NewWriteRequestWritesWithDefaults() *WriteRequestWrites { this := WriteRequestWrites{} + var onDuplicate = "error" + this.OnDuplicate = &onDuplicate return &this } @@ -65,9 +71,44 @@ func (o *WriteRequestWrites) SetTupleKeys(v []TupleKey) { o.TupleKeys = v } +// GetOnDuplicate returns the OnDuplicate field value if set, zero value otherwise. +func (o *WriteRequestWrites) GetOnDuplicate() string { + if o == nil || o.OnDuplicate == nil { + var ret string + return ret + } + return *o.OnDuplicate +} + +// GetOnDuplicateOk returns a tuple with the OnDuplicate field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *WriteRequestWrites) GetOnDuplicateOk() (*string, bool) { + if o == nil || o.OnDuplicate == nil { + return nil, false + } + return o.OnDuplicate, true +} + +// HasOnDuplicate returns a boolean if a field has been set. +func (o *WriteRequestWrites) HasOnDuplicate() bool { + if o != nil && o.OnDuplicate != nil { + return true + } + + return false +} + +// SetOnDuplicate gets a reference to the given string and assigns it to the OnDuplicate field. +func (o *WriteRequestWrites) SetOnDuplicate(v string) { + o.OnDuplicate = &v +} + func (o WriteRequestWrites) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} toSerialize["tuple_keys"] = o.TupleKeys + if o.OnDuplicate != nil { + toSerialize["on_duplicate"] = o.OnDuplicate + } var b bytes.Buffer enc := json.NewEncoder(&b) enc.SetEscapeHTML(false) diff --git a/models_test.go b/models_test.go new file mode 100644 index 0000000..db89f99 --- /dev/null +++ b/models_test.go @@ -0,0 +1,770 @@ +/** + * Go SDK for OpenFGA + * + * API version: 1.x + * Website: https://openfga.dev + * Documentation: https://openfga.dev/docs + * Support: https://openfga.dev/community + * License: [Apache-2.0](https://github.com/openfga/go-sdk/blob/main/LICENSE) + * + * NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. + */ + +package openfga + +import ( + "encoding/json" + "testing" +) + +func TestWriteRequestDeletes(t *testing.T) { + t.Run("NewWriteRequestDeletes sets default OnMissing", func(t *testing.T) { + tupleKeys := []TupleKeyWithoutCondition{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + deletes := NewWriteRequestDeletes(tupleKeys) + + if deletes.OnMissing == nil { + t.Fatalf("Expected OnMissing to be non-nil with default value") + } + + if *deletes.OnMissing != "error" { + t.Fatalf("Expected OnMissing default to be 'error', got %v", *deletes.OnMissing) + } + + if len(deletes.TupleKeys) != 1 { + t.Fatalf("Expected 1 tuple key, got %d", len(deletes.TupleKeys)) + } + }) + + t.Run("NewWriteRequestDeletesWithDefaults sets default OnMissing", func(t *testing.T) { + deletes := NewWriteRequestDeletesWithDefaults() + + if deletes.OnMissing == nil { + t.Fatalf("Expected OnMissing to be non-nil with default value") + } + + if *deletes.OnMissing != "error" { + t.Fatalf("Expected OnMissing default to be 'error', got %v", *deletes.OnMissing) + } + }) + + t.Run("GetOnMissing returns correct value", func(t *testing.T) { + deletes := WriteRequestDeletes{} + + // Test when OnMissing is nil + if deletes.GetOnMissing() != "" { + t.Fatalf("Expected empty string when OnMissing is nil, got %v", deletes.GetOnMissing()) + } + + // Test when OnMissing is set + onMiss := "ignore" + deletes.OnMissing = &onMiss + + if deletes.GetOnMissing() != "ignore" { + t.Fatalf("Expected 'ignore', got %v", deletes.GetOnMissing()) + } + }) + + t.Run("GetOnMissingOk returns correct value and status", func(t *testing.T) { + deletes := WriteRequestDeletes{} + + // Test when OnMissing is nil + value, ok := deletes.GetOnMissingOk() + if value != nil || ok { + t.Fatalf("Expected nil value and false status when OnMissing is nil, got %v, %v", value, ok) + } + + // Test when OnMissing is set + onMiss := "ignore" + deletes.OnMissing = &onMiss + + value, ok = deletes.GetOnMissingOk() + if value == nil || !ok || *value != "ignore" { + t.Fatalf("Expected 'ignore' value and true status, got %v, %v", value, ok) + } + }) + + t.Run("HasOnMissing returns correct status", func(t *testing.T) { + deletes := WriteRequestDeletes{} + + // Test when OnMissing is nil + if deletes.HasOnMissing() { + t.Fatalf("Expected false when OnMissing is nil") + } + + // Test when OnMissing is set + onMiss := "ignore" + deletes.OnMissing = &onMiss + + if !deletes.HasOnMissing() { + t.Fatalf("Expected true when OnMissing is set") + } + }) + + t.Run("SetOnMissing sets value correctly", func(t *testing.T) { + deletes := WriteRequestDeletes{} + + deletes.SetOnMissing("ignore") + + if deletes.OnMissing == nil || *deletes.OnMissing != "ignore" { + t.Fatalf("Expected OnMissing to be 'ignore', got %v", deletes.OnMissing) + } + }) + + t.Run("MarshalJSON includes OnMissing when set", func(t *testing.T) { + tupleKeys := []TupleKeyWithoutCondition{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + deletes := WriteRequestDeletes{ + TupleKeys: tupleKeys, + } + + // Test without OnMissing + jsonData, err := deletes.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling JSON: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if _, exists := result["on_missing"]; exists { + t.Fatalf("Expected on_missing to not be present when nil") + } + + // Test with OnMissing + onMiss := "ignore" + deletes.OnMissing = &onMiss + + jsonData, err = deletes.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling JSON: %v", err) + } + + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if result["on_missing"] != "ignore" { + t.Fatalf("Expected on_missing to be 'ignore', got %v", result["on_missing"]) + } + }) + + t.Run("OnMissing validation - valid values", func(t *testing.T) { + validValues := []string{"error", "ignore"} + + for _, value := range validValues { + deletes := WriteRequestDeletes{} + deletes.SetOnMissing(value) + + if deletes.GetOnMissing() != value { + t.Fatalf("Expected OnMissing to be '%s', got %v", value, deletes.GetOnMissing()) + } + } + }) +} + +func TestWriteRequestWrites(t *testing.T) { + t.Run("NewWriteRequestWrites sets default OnDuplicate", func(t *testing.T) { + tupleKeys := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + writes := NewWriteRequestWrites(tupleKeys) + + if writes.OnDuplicate == nil { + t.Fatalf("Expected OnDuplicate to be non-nil with default value") + } + + if *writes.OnDuplicate != "error" { + t.Fatalf("Expected OnDuplicate default to be 'error', got %v", *writes.OnDuplicate) + } + + if len(writes.TupleKeys) != 1 { + t.Fatalf("Expected 1 tuple key, got %d", len(writes.TupleKeys)) + } + }) + + t.Run("NewWriteRequestWritesWithDefaults sets default OnDuplicate", func(t *testing.T) { + writes := NewWriteRequestWritesWithDefaults() + + if writes.OnDuplicate == nil { + t.Fatalf("Expected OnDuplicate to be non-nil with default value") + } + + if *writes.OnDuplicate != "error" { + t.Fatalf("Expected OnDuplicate default to be 'error', got %v", *writes.OnDuplicate) + } + }) + + t.Run("GetOnDuplicate returns correct value", func(t *testing.T) { + writes := WriteRequestWrites{} + + // Test when OnDuplicate is nil + if writes.GetOnDuplicate() != "" { + t.Fatalf("Expected empty string when OnDuplicate is nil, got %v", writes.GetOnDuplicate()) + } + + // Test when OnDuplicate is set + onDup := "ignore" + writes.OnDuplicate = &onDup + + if writes.GetOnDuplicate() != "ignore" { + t.Fatalf("Expected 'ignore', got %v", writes.GetOnDuplicate()) + } + }) + + t.Run("GetOnDuplicateOk returns correct value and status", func(t *testing.T) { + writes := WriteRequestWrites{} + + // Test when OnDuplicate is nil + value, ok := writes.GetOnDuplicateOk() + if value != nil || ok { + t.Fatalf("Expected nil value and false status when OnDuplicate is nil, got %v, %v", value, ok) + } + + // Test when OnDuplicate is set + onDup := "ignore" + writes.OnDuplicate = &onDup + + value, ok = writes.GetOnDuplicateOk() + if value == nil || !ok || *value != "ignore" { + t.Fatalf("Expected 'ignore' value and true status, got %v, %v", value, ok) + } + }) + + t.Run("HasOnDuplicate returns correct status", func(t *testing.T) { + writes := WriteRequestWrites{} + + // Test when OnDuplicate is nil + if writes.HasOnDuplicate() { + t.Fatalf("Expected false when OnDuplicate is nil") + } + + // Test when OnDuplicate is set + onDup := "ignore" + writes.OnDuplicate = &onDup + + if !writes.HasOnDuplicate() { + t.Fatalf("Expected true when OnDuplicate is set") + } + }) + + t.Run("SetOnDuplicate sets value correctly", func(t *testing.T) { + writes := WriteRequestWrites{} + + writes.SetOnDuplicate("ignore") + + if writes.OnDuplicate == nil || *writes.OnDuplicate != "ignore" { + t.Fatalf("Expected OnDuplicate to be 'ignore', got %v", writes.OnDuplicate) + } + }) + + t.Run("MarshalJSON includes OnDuplicate when set", func(t *testing.T) { + tupleKeys := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + writes := WriteRequestWrites{ + TupleKeys: tupleKeys, + } + + // Test without OnDuplicate + jsonData, err := writes.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling JSON: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if _, exists := result["on_duplicate"]; exists { + t.Fatalf("Expected on_duplicate to not be present when nil") + } + + // Test with OnDuplicate + onDup := "ignore" + writes.OnDuplicate = &onDup + + jsonData, err = writes.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling JSON: %v", err) + } + + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if result["on_duplicate"] != "ignore" { + t.Fatalf("Expected on_duplicate to be 'ignore', got %v", result["on_duplicate"]) + } + }) + + t.Run("OnDuplicate validation - valid values", func(t *testing.T) { + validValues := []string{"error", "ignore"} + + for _, value := range validValues { + writes := WriteRequestWrites{} + writes.SetOnDuplicate(value) + + if writes.GetOnDuplicate() != value { + t.Fatalf("Expected OnDuplicate to be '%s', got %v", value, writes.GetOnDuplicate()) + } + } + }) +} + +func TestWriteRequest(t *testing.T) { + t.Run("NewWriteRequest creates empty WriteRequest", func(t *testing.T) { + writeRequest := NewWriteRequest() + + if writeRequest == nil { + t.Fatalf("Expected non-nil WriteRequest") + } + + if writeRequest.Writes != nil { + t.Fatalf("Expected Writes to be nil by default") + } + + if writeRequest.Deletes != nil { + t.Fatalf("Expected Deletes to be nil by default") + } + + if writeRequest.AuthorizationModelId != nil { + t.Fatalf("Expected AuthorizationModelId to be nil by default") + } + }) + + t.Run("NewWriteRequestWithDefaults creates empty WriteRequest", func(t *testing.T) { + writeRequest := NewWriteRequestWithDefaults() + + if writeRequest == nil { + t.Fatalf("Expected non-nil WriteRequest") + } + + if writeRequest.Writes != nil { + t.Fatalf("Expected Writes to be nil by default") + } + + if writeRequest.Deletes != nil { + t.Fatalf("Expected Deletes to be nil by default") + } + + if writeRequest.AuthorizationModelId != nil { + t.Fatalf("Expected AuthorizationModelId to be nil by default") + } + }) + + t.Run("Writes field operations", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test GetWrites when nil + writes := writeRequest.GetWrites() + if writes.TupleKeys != nil { + t.Fatalf("Expected empty WriteRequestWrites when Writes is nil") + } + + // Test GetWritesOk when nil + writesPtr, ok := writeRequest.GetWritesOk() + if writesPtr != nil || ok { + t.Fatalf("Expected nil pointer and false when Writes is nil, got %v, %v", writesPtr, ok) + } + + // Test HasWrites when nil + if writeRequest.HasWrites() { + t.Fatalf("Expected false when Writes is nil") + } + + // Test SetWrites + tupleKeys := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + writesValue := WriteRequestWrites{TupleKeys: tupleKeys} + writeRequest.SetWrites(writesValue) + + // Test GetWrites after setting + retrievedWrites := writeRequest.GetWrites() + if len(retrievedWrites.TupleKeys) != 1 { + t.Fatalf("Expected 1 tuple key, got %d", len(retrievedWrites.TupleKeys)) + } + + // Test GetWritesOk after setting + writesPtr, ok = writeRequest.GetWritesOk() + if writesPtr == nil || !ok || len(writesPtr.TupleKeys) != 1 { + t.Fatalf("Expected valid pointer and true after setting Writes") + } + + // Test HasWrites after setting + if !writeRequest.HasWrites() { + t.Fatalf("Expected true after setting Writes") + } + }) + + t.Run("Deletes field operations", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test GetDeletes when nil + deletes := writeRequest.GetDeletes() + if deletes.TupleKeys != nil { + t.Fatalf("Expected empty WriteRequestDeletes when Deletes is nil") + } + + // Test GetDeletesOk when nil + deletesPtr, ok := writeRequest.GetDeletesOk() + if deletesPtr != nil || ok { + t.Fatalf("Expected nil pointer and false when Deletes is nil, got %v, %v", deletesPtr, ok) + } + + // Test HasDeletes when nil + if writeRequest.HasDeletes() { + t.Fatalf("Expected false when Deletes is nil") + } + + // Test SetDeletes + tupleKeys := []TupleKeyWithoutCondition{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + deletesValue := WriteRequestDeletes{TupleKeys: tupleKeys} + writeRequest.SetDeletes(deletesValue) + + // Test GetDeletes after setting + retrievedDeletes := writeRequest.GetDeletes() + if len(retrievedDeletes.TupleKeys) != 1 { + t.Fatalf("Expected 1 tuple key, got %d", len(retrievedDeletes.TupleKeys)) + } + + // Test GetDeletesOk after setting + deletesPtr, ok = writeRequest.GetDeletesOk() + if deletesPtr == nil || !ok || len(deletesPtr.TupleKeys) != 1 { + t.Fatalf("Expected valid pointer and true after setting Deletes") + } + + // Test HasDeletes after setting + if !writeRequest.HasDeletes() { + t.Fatalf("Expected true after setting Deletes") + } + }) + + t.Run("AuthorizationModelId field operations", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test GetAuthorizationModelId when nil + modelId := writeRequest.GetAuthorizationModelId() + if modelId != "" { + t.Fatalf("Expected empty string when AuthorizationModelId is nil, got %v", modelId) + } + + // Test GetAuthorizationModelIdOk when nil + modelIdPtr, ok := writeRequest.GetAuthorizationModelIdOk() + if modelIdPtr != nil || ok { + t.Fatalf("Expected nil pointer and false when AuthorizationModelId is nil, got %v, %v", modelIdPtr, ok) + } + + // Test HasAuthorizationModelId when nil + if writeRequest.HasAuthorizationModelId() { + t.Fatalf("Expected false when AuthorizationModelId is nil") + } + + // Test SetAuthorizationModelId + expectedModelId := "01GAHCE4YVKPQEKZQHT2R89MQV" + writeRequest.SetAuthorizationModelId(expectedModelId) + + // Test GetAuthorizationModelId after setting + retrievedModelId := writeRequest.GetAuthorizationModelId() + if retrievedModelId != expectedModelId { + t.Fatalf("Expected %s, got %s", expectedModelId, retrievedModelId) + } + + // Test GetAuthorizationModelIdOk after setting + modelIdPtr, ok = writeRequest.GetAuthorizationModelIdOk() + if modelIdPtr == nil || !ok || *modelIdPtr != expectedModelId { + t.Fatalf("Expected valid pointer and true after setting AuthorizationModelId") + } + + // Test HasAuthorizationModelId after setting + if !writeRequest.HasAuthorizationModelId() { + t.Fatalf("Expected true after setting AuthorizationModelId") + } + }) + + t.Run("MarshalJSON includes all fields when set", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test marshaling empty request + jsonData, err := writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling empty WriteRequest: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if len(result) != 0 { + t.Fatalf("Expected empty object for empty WriteRequest, got %v", result) + } + + // Test marshaling with all fields set + tupleKeysWrite := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + tupleKeysDelete := []TupleKeyWithoutCondition{ + { + User: "user:another-user", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + onDuplicate := "ignore" + onMissing := "ignore" + writes := WriteRequestWrites{TupleKeys: tupleKeysWrite, OnDuplicate: &onDuplicate} + deletes := WriteRequestDeletes{TupleKeys: tupleKeysDelete, OnMissing: &onMissing} + modelId := "01GAHCE4YVKPQEKZQHT2R89MQV" + + writeRequest.SetWrites(writes) + writeRequest.SetDeletes(deletes) + writeRequest.SetAuthorizationModelId(modelId) + + jsonData, err = writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling complete WriteRequest: %v", err) + } + + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + // Verify all fields are present + if _, exists := result["writes"]; !exists { + t.Fatalf("Expected 'writes' field in JSON") + } + + if _, exists := result["deletes"]; !exists { + t.Fatalf("Expected 'deletes' field in JSON") + } + + if _, exists := result["authorization_model_id"]; !exists { + t.Fatalf("Expected 'authorization_model_id' field in JSON") + } + + if result["authorization_model_id"] != modelId { + t.Fatalf("Expected authorization_model_id to be %s, got %v", modelId, result["authorization_model_id"]) + } + + // Verify nested conflict options are preserved + writesMap := result["writes"].(map[string]interface{}) + if writesMap["on_duplicate"] != "ignore" { + t.Fatalf("Expected writes.on_duplicate to be 'ignore', got %v", writesMap["on_duplicate"]) + } + + deletesMap := result["deletes"].(map[string]interface{}) + if deletesMap["on_missing"] != "ignore" { + t.Fatalf("Expected deletes.on_missing to be 'ignore', got %v", deletesMap["on_missing"]) + } + }) + + t.Run("WriteRequest with only writes", func(t *testing.T) { + writeRequest := WriteRequest{} + + tupleKeys := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + onDuplicate := "error" + writes := WriteRequestWrites{TupleKeys: tupleKeys, OnDuplicate: &onDuplicate} + writeRequest.SetWrites(writes) + + // Verify only writes is set + if !writeRequest.HasWrites() { + t.Fatalf("Expected HasWrites to be true") + } + + if writeRequest.HasDeletes() { + t.Fatalf("Expected HasDeletes to be false") + } + + if writeRequest.HasAuthorizationModelId() { + t.Fatalf("Expected HasAuthorizationModelId to be false") + } + + // Test JSON marshaling + jsonData, err := writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling WriteRequest: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if _, exists := result["writes"]; !exists { + t.Fatalf("Expected 'writes' field in JSON") + } + + if _, exists := result["deletes"]; exists { + t.Fatalf("Expected 'deletes' field to not be in JSON") + } + + if _, exists := result["authorization_model_id"]; exists { + t.Fatalf("Expected 'authorization_model_id' field to not be in JSON") + } + }) + + t.Run("WriteRequest with only deletes", func(t *testing.T) { + writeRequest := WriteRequest{} + + tupleKeys := []TupleKeyWithoutCondition{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + onMissing := "error" + deletes := WriteRequestDeletes{TupleKeys: tupleKeys, OnMissing: &onMissing} + writeRequest.SetDeletes(deletes) + + // Verify only deletes is set + if writeRequest.HasWrites() { + t.Fatalf("Expected HasWrites to be false") + } + + if !writeRequest.HasDeletes() { + t.Fatalf("Expected HasDeletes to be true") + } + + if writeRequest.HasAuthorizationModelId() { + t.Fatalf("Expected HasAuthorizationModelId to be false") + } + + // Test JSON marshaling + jsonData, err := writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling WriteRequest: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if _, exists := result["writes"]; exists { + t.Fatalf("Expected 'writes' field to not be in JSON") + } + + if _, exists := result["deletes"]; !exists { + t.Fatalf("Expected 'deletes' field in JSON") + } + + if _, exists := result["authorization_model_id"]; exists { + t.Fatalf("Expected 'authorization_model_id' field to not be in JSON") + } + }) + + t.Run("WriteRequest with conflict options integration", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test with writes having ignore duplicate policy + tupleKeysWrite := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + // Test with deletes having ignore missing policy + tupleKeysDelete := []TupleKeyWithoutCondition{ + { + User: "user:another-user", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + onDuplicate := "ignore" + onMissing := "ignore" + + writes := WriteRequestWrites{TupleKeys: tupleKeysWrite, OnDuplicate: &onDuplicate} + deletes := WriteRequestDeletes{TupleKeys: tupleKeysDelete, OnMissing: &onMissing} + + writeRequest.SetWrites(writes) + writeRequest.SetDeletes(deletes) + writeRequest.SetAuthorizationModelId("01GAHCE4YVKPQEKZQHT2R89MQV") + + // Verify conflict options are preserved through getter methods + retrievedWrites := writeRequest.GetWrites() + if retrievedWrites.GetOnDuplicate() != "ignore" { + t.Fatalf("Expected OnDuplicate to be 'ignore', got %v", retrievedWrites.GetOnDuplicate()) + } + + retrievedDeletes := writeRequest.GetDeletes() + if retrievedDeletes.GetOnMissing() != "ignore" { + t.Fatalf("Expected OnMissing to be 'ignore', got %v", retrievedDeletes.GetOnMissing()) + } + + // Verify JSON serialization preserves conflict options + jsonData, err := writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling WriteRequest: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + writesMap := result["writes"].(map[string]interface{}) + if writesMap["on_duplicate"] != "ignore" { + t.Fatalf("Expected writes.on_duplicate to be 'ignore' in JSON, got %v", writesMap["on_duplicate"]) + } + + deletesMap := result["deletes"].(map[string]interface{}) + if deletesMap["on_missing"] != "ignore" { + t.Fatalf("Expected deletes.on_missing to be 'ignore' in JSON, got %v", deletesMap["on_missing"]) + } + }) +}