From a1f7cbbc9004e786aa05c9b911b665c3a003e401 Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Wed, 22 Jan 2025 11:40:53 -0600 Subject: [PATCH 1/4] feat: support start time parameter on read changes feat: support start time parameter on read changes --- README.md | 3 ++- cmd/tuple/changes.go | 37 ++++++++++++++++++++++++++++++------ cmd/tuple/changes_test.go | 40 +++++++++++++++++++++++++++++++-------- go.mod | 2 +- go.sum | 2 ++ 5 files changed, 68 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b38ff512..26e2a659 100644 --- a/README.md +++ b/README.md @@ -655,7 +655,7 @@ type document | [Write Relationship Tuples](#write-relationship-tuples) | `write` | `--store-id`, `--model-id` | `fga tuple write user:anne can_view document:roadmap --store-id=01H0H015178Y2V4CX10C2KGHF4` | | [Delete Relationship Tuples](#delete-relationship-tuples) | `delete` | `--store-id`, `--model-id` | `fga tuple delete user:anne can_view document:roadmap --store-id=01H0H015178Y2V4CX10C2KGHF4` | | [Read Relationship Tuples](#read-relationship-tuples) | `read` | `--store-id`, `--model-id` | `fga tuple read --store-id=01H0H015178Y2V4CX10C2KGHF4 --model-id=01GXSA8YR785C4FYS3C0RTG7B1` | -| [Read Relationship Tuple Changes (Watch)](#read-relationship-tuple-changes-watch) | `changes` | `--store-id`, `--type`, `--continuation-token`, | `fga tuple changes --store-id=01H0H015178Y2V4CX10C2KGHF4 --type=document --continuation-token=M3w=` | +| [Read Relationship Tuple Changes (Watch)](#read-relationship-tuple-changes-watch) | `changes` | `--store-id`, `--type`, `--start-time`, `--continuation-token`, | `fga tuple changes --store-id=01H0H015178Y2V4CX10C2KGHF4 --type=document --start-time=2022-01-01T00:00:00Z --continuation-token=M3w=` | | [Import Relationship Tuples](#import-relationship-tuples) | `import` | `--store-id`, `--model-id`, `--file` | `fga tuple import --store-id=01H0H015178Y2V4CX10C2KGHF4 --model-id=01GXSA8YR785C4FYS3C0RTG7B1 --file tuples.json` | ##### Write Relationship Tuples @@ -896,6 +896,7 @@ fga tuple **changes** --type --store-id= ###### Parameters * `--store-id`: Specifies the store id * `--type`: Restrict to a specific type (optional) +* `--start-time`: Return changes since a specified time * `--max-pages`: Max number of pages to retrieve (default: 20) * `--continuation-token`: Continuation token to start changes from diff --git a/cmd/tuple/changes.go b/cmd/tuple/changes.go index 1a3fab08..538dae98 100644 --- a/cmd/tuple/changes.go +++ b/cmd/tuple/changes.go @@ -19,6 +19,7 @@ package tuple import ( "context" "fmt" + "time" openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" @@ -32,15 +33,32 @@ import ( var MaxReadChangesPagesLength = 20 func readChanges( - fgaClient client.SdkClient, maxPages int, selectedType string, continuationToken string, + fgaClient client.SdkClient, maxPages int, selectedType string, startTime string, continuationToken string, ) (*openfga.ReadChangesResponse, error) { changes := []openfga.TupleChange{} pageIndex := 0 + var startTimeObj *time.Time + + if startTime != "" { + layout := "2006-01-02T15:04:05Z" + + parsedTime, err := time.Parse(layout, startTime) + if err != nil { + return nil, fmt.Errorf("failed to parse startTime: %w", err) + } + + startTimeObj = &parsedTime + } + for { body := &client.ClientReadChangesRequest{ Type: selectedType, } + if startTimeObj != nil { + body.StartTime = *startTimeObj + } + options := &client.ClientReadChangesOptions{ ContinuationToken: &continuationToken, } @@ -67,10 +85,11 @@ func readChanges( // changesCmd represents the changes command. var changesCmd = &cobra.Command{ - Use: "changes", - Short: "Read Relationship Tuple Changes (Watch)", - Long: "Get a list of relationship tuple changes (Writes and Deletes) across time.", - Example: "fga tuple changes --store-id=01H0H015178Y2V4CX10C2KGHF4 --type document --continuation-token=MXw=", + Use: "changes", + Short: "Read Relationship Tuple Changes (Watch)", + Long: "Get a list of relationship tuple changes (Writes and Deletes) across time.", + Example: `fga tuple changes --store-id=01H0H015178Y2V4CX10C2KGHF4 --type document + --start-time 2022-01-01T00:00:00Z --continuation-token=MXw=`, RunE: func(cmd *cobra.Command, _ []string) error { clientConfig := cmdutils.GetClientConfig(cmd) @@ -89,12 +108,17 @@ var changesCmd = &cobra.Command{ return fmt.Errorf("failed to get tuple changes due to %w", err) } + startTime, err := cmd.Flags().GetString("start-time") + if err != nil { + return fmt.Errorf("failed to get tuple changes due to %w", err) + } + continuationToken, err := cmd.Flags().GetString("continuation-token") if err != nil { return fmt.Errorf("failed to get tuple changes due to %w", err) } - response, err := readChanges(fgaClient, maxPages, selectedType, continuationToken) + response, err := readChanges(fgaClient, maxPages, selectedType, startTime, continuationToken) if err != nil { return err } @@ -105,6 +129,7 @@ var changesCmd = &cobra.Command{ func init() { changesCmd.Flags().String("type", "", "Type to restrict the changes by.") + changesCmd.Flags().String("start-time", "", "Time to return changes since.") changesCmd.Flags().Int("max-pages", MaxReadChangesPagesLength, "Max number of pages to get.") changesCmd.Flags().String("continuation-token", "", "Continuation token to start changes from.") } diff --git a/cmd/tuple/changes_test.go b/cmd/tuple/changes_test.go index 6dbfbff6..fbecc86d 100644 --- a/cmd/tuple/changes_test.go +++ b/cmd/tuple/changes_test.go @@ -44,7 +44,7 @@ func TestReadChangesError(t *testing.T) { mockFgaClient.EXPECT().ReadChanges(context.Background()).Return(mockBody) - _, err := readChanges(mockFgaClient, 5, "document", "") + _, err := readChanges(mockFgaClient, 5, "document", "", "") if err == nil { t.Error("Expect error but there is none") } @@ -82,7 +82,7 @@ func TestReadChangesEmpty(t *testing.T) { mockFgaClient.EXPECT().ReadChanges(context.Background()).Return(mockBody) - output, err := readChanges(mockFgaClient, 5, "document", "") + output, err := readChanges(mockFgaClient, 5, "document", "", "") if err != nil { t.Error(err) } @@ -102,6 +102,8 @@ func TestReadChangesEmpty(t *testing.T) { func TestReadChangesSinglePage(t *testing.T) { t.Parallel() + const layout = "2006-01-02T15:04:05Z" + mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) @@ -139,14 +141,20 @@ func TestReadChangesSinglePage(t *testing.T) { mockBody := mock_client.NewMockSdkClientReadChangesRequestInterface(mockCtrl) + sTime, err := time.Parse(layout, "2022-01-01T00:00:00Z") + if err != nil { + t.Error(err) + } + body := client.ClientReadChangesRequest{ - Type: "document", + Type: "document", + StartTime: sTime, } mockBody.EXPECT().Body(body).Return(mockRequest) mockFgaClient.EXPECT().ReadChanges(context.Background()).Return(mockBody) - output, err := readChanges(mockFgaClient, 5, "document", "") + output, err := readChanges(mockFgaClient, 5, "document", "2022-01-01T00:00:00Z", "") if err != nil { t.Error(err) } @@ -166,6 +174,8 @@ func TestReadChangesSinglePage(t *testing.T) { func TestReadChangesMultiPages(t *testing.T) { t.Parallel() + const layout = "2006-01-02T15:04:05Z" + mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -232,8 +242,14 @@ func TestReadChangesMultiPages(t *testing.T) { mockBody1 := mock_client.NewMockSdkClientReadChangesRequestInterface(mockCtrl) mockBody2 := mock_client.NewMockSdkClientReadChangesRequestInterface(mockCtrl) + sTime, err := time.Parse(layout, "2022-01-01T00:00:00Z") + if err != nil { + t.Error(err) + } + body := client.ClientReadChangesRequest{ - Type: "document", + Type: "document", + StartTime: sTime, } gomock.InOrder( @@ -246,7 +262,7 @@ func TestReadChangesMultiPages(t *testing.T) { mockFgaClient.EXPECT().ReadChanges(context.Background()).Return(mockBody2), ) - output, err := readChanges(mockFgaClient, 5, "document", "") + output, err := readChanges(mockFgaClient, 5, "document", "2022-01-01T00:00:00Z", "") if err != nil { t.Error(err) } @@ -266,6 +282,8 @@ func TestReadChangesMultiPages(t *testing.T) { func TestReadChangesMultiPagesLimit(t *testing.T) { t.Parallel() + const layout = "2006-01-02T15:04:05Z" + mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) @@ -303,14 +321,20 @@ func TestReadChangesMultiPagesLimit(t *testing.T) { mockBody := mock_client.NewMockSdkClientReadChangesRequestInterface(mockCtrl) + sTime, err := time.Parse(layout, "2022-01-01T00:00:00Z") + if err != nil { + t.Error(err) + } + body := client.ClientReadChangesRequest{ - Type: "document", + Type: "document", + StartTime: sTime, } mockBody.EXPECT().Body(body).Return(mockRequest) mockFgaClient.EXPECT().ReadChanges(context.Background()).Return(mockBody) - output, err := readChanges(mockFgaClient, 1, "document", "") + output, err := readChanges(mockFgaClient, 1, "document", "2022-01-01T00:00:00Z", "") if err != nil { t.Error(err) } diff --git a/go.mod b/go.mod index 8d49505d..9707dfb4 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/nwidger/jsoncolor v0.3.2 github.com/oklog/ulid/v2 v2.1.0 github.com/openfga/api/proto v0.0.0-20250107154247-c22e6db5c4f5 - github.com/openfga/go-sdk v0.6.4-0.20250107171931-2adebcc8c8bc + github.com/openfga/go-sdk v0.6.4 github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c github.com/openfga/openfga v1.8.4 github.com/spf13/cobra v1.8.1 diff --git a/go.sum b/go.sum index e9000ef7..9c941662 100644 --- a/go.sum +++ b/go.sum @@ -182,6 +182,8 @@ github.com/openfga/api/proto v0.0.0-20250107154247-c22e6db5c4f5 h1:z9jaRoo+NIN1A github.com/openfga/api/proto v0.0.0-20250107154247-c22e6db5c4f5/go.mod h1:m74TNgnAAIJ03gfHcx+xaRWnr+IbQy3y/AVNwwCFrC0= github.com/openfga/go-sdk v0.6.4-0.20250107171931-2adebcc8c8bc h1:E7x5UZbNIbcCOhBRD3cqrmQz35qNNceMnZPb6UxZRnw= github.com/openfga/go-sdk v0.6.4-0.20250107171931-2adebcc8c8bc/go.mod h1:zui7pHE3eLAYh2fFmEMrWg9XbxYns2WW5Xr/GEgili4= +github.com/openfga/go-sdk v0.6.4 h1:VAeH9exZuG1qaBt41+mjIK5RxTtuL9maS4d47YsYGZw= +github.com/openfga/go-sdk v0.6.4/go.mod h1:zui7pHE3eLAYh2fFmEMrWg9XbxYns2WW5Xr/GEgili4= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c h1:1y84C0V4NRfPtRi4MqQ7+gnFtYgeBKPIeIAPLdVJ7j4= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c/go.mod h1:12RMe/HuRNyOzS33RQa53jwdcxE2znr8ycXMlVbgQN4= github.com/openfga/openfga v1.8.4 h1:OqyRpuxMCxcS7irTFYFkhAIYzmAnczNwxUqjnuZOQyo= From 17d722daa5108f334a1f9366ab22b196d42b4ef4 Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Fri, 31 Jan 2025 11:05:19 -0600 Subject: [PATCH 2/4] Update README.md Co-authored-by: Ewan Harris --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26e2a659..44b3cce5 100644 --- a/README.md +++ b/README.md @@ -896,7 +896,7 @@ fga tuple **changes** --type --store-id= ###### Parameters * `--store-id`: Specifies the store id * `--type`: Restrict to a specific type (optional) -* `--start-time`: Return changes since a specified time +* `--start-time`: Return changes since a specified time (optional) * `--max-pages`: Max number of pages to retrieve (default: 20) * `--continuation-token`: Continuation token to start changes from From f165df16383cb01dae080a1eff97cd8a1f7b3c74 Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Wed, 5 Feb 2025 08:54:18 -0600 Subject: [PATCH 3/4] chore: bump go-sdk version to include start_time variable fix --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 9707dfb4..ed835ea9 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/nwidger/jsoncolor v0.3.2 github.com/oklog/ulid/v2 v2.1.0 github.com/openfga/api/proto v0.0.0-20250107154247-c22e6db5c4f5 - github.com/openfga/go-sdk v0.6.4 + github.com/openfga/go-sdk v0.6.5-0.20250205144545-22df3ecba077 github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c github.com/openfga/openfga v1.8.4 github.com/spf13/cobra v1.8.1 diff --git a/go.sum b/go.sum index 9c941662..6abc15f0 100644 --- a/go.sum +++ b/go.sum @@ -184,6 +184,8 @@ github.com/openfga/go-sdk v0.6.4-0.20250107171931-2adebcc8c8bc h1:E7x5UZbNIbcCOh github.com/openfga/go-sdk v0.6.4-0.20250107171931-2adebcc8c8bc/go.mod h1:zui7pHE3eLAYh2fFmEMrWg9XbxYns2WW5Xr/GEgili4= github.com/openfga/go-sdk v0.6.4 h1:VAeH9exZuG1qaBt41+mjIK5RxTtuL9maS4d47YsYGZw= github.com/openfga/go-sdk v0.6.4/go.mod h1:zui7pHE3eLAYh2fFmEMrWg9XbxYns2WW5Xr/GEgili4= +github.com/openfga/go-sdk v0.6.5-0.20250205144545-22df3ecba077 h1:o1s2Y5cjg/ALF9vN2BiTcF6ucrGDGfJKR7N8QdjKcR4= +github.com/openfga/go-sdk v0.6.5-0.20250205144545-22df3ecba077/go.mod h1:zui7pHE3eLAYh2fFmEMrWg9XbxYns2WW5Xr/GEgili4= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c h1:1y84C0V4NRfPtRi4MqQ7+gnFtYgeBKPIeIAPLdVJ7j4= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c/go.mod h1:12RMe/HuRNyOzS33RQa53jwdcxE2znr8ycXMlVbgQN4= github.com/openfga/openfga v1.8.4 h1:OqyRpuxMCxcS7irTFYFkhAIYzmAnczNwxUqjnuZOQyo= From b00b8b30d871b5b1215901e70a19986953810e58 Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Wed, 5 Feb 2025 13:23:42 -0600 Subject: [PATCH 4/4] chore: bump go-sdk version, update changelog feat: use RFC3339 in place of ISO8601 layout string fix: resolve linting error from newline docs: Update changelog with start time and prep for 0.6.4 release --- CHANGELOG.md | 4 ++++ cmd/tuple/changes.go | 4 +--- cmd/tuple/changes_test.go | 12 +++--------- go.mod | 2 +- go.sum | 2 ++ 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 785c27bf..ddceb569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +#### [Unreleased](https://github.com/openfga/cli/compare/v0.6.3...HEAD) (2025-02-06) +Added: +- Support for start-time parameter in changes command (#443) + #### [0.6.3](https://github.com/openfga/cli/compare/v0.6.2...v0.6.3) (2025-01-22) Added: diff --git a/cmd/tuple/changes.go b/cmd/tuple/changes.go index 538dae98..055c30b2 100644 --- a/cmd/tuple/changes.go +++ b/cmd/tuple/changes.go @@ -41,9 +41,7 @@ func readChanges( var startTimeObj *time.Time if startTime != "" { - layout := "2006-01-02T15:04:05Z" - - parsedTime, err := time.Parse(layout, startTime) + parsedTime, err := time.Parse(time.RFC3339, startTime) if err != nil { return nil, fmt.Errorf("failed to parse startTime: %w", err) } diff --git a/cmd/tuple/changes_test.go b/cmd/tuple/changes_test.go index fbecc86d..b4c82921 100644 --- a/cmd/tuple/changes_test.go +++ b/cmd/tuple/changes_test.go @@ -102,8 +102,6 @@ func TestReadChangesEmpty(t *testing.T) { func TestReadChangesSinglePage(t *testing.T) { t.Parallel() - const layout = "2006-01-02T15:04:05Z" - mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) @@ -141,7 +139,7 @@ func TestReadChangesSinglePage(t *testing.T) { mockBody := mock_client.NewMockSdkClientReadChangesRequestInterface(mockCtrl) - sTime, err := time.Parse(layout, "2022-01-01T00:00:00Z") + sTime, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z") if err != nil { t.Error(err) } @@ -174,8 +172,6 @@ func TestReadChangesSinglePage(t *testing.T) { func TestReadChangesMultiPages(t *testing.T) { t.Parallel() - const layout = "2006-01-02T15:04:05Z" - mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -242,7 +238,7 @@ func TestReadChangesMultiPages(t *testing.T) { mockBody1 := mock_client.NewMockSdkClientReadChangesRequestInterface(mockCtrl) mockBody2 := mock_client.NewMockSdkClientReadChangesRequestInterface(mockCtrl) - sTime, err := time.Parse(layout, "2022-01-01T00:00:00Z") + sTime, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z") if err != nil { t.Error(err) } @@ -282,8 +278,6 @@ func TestReadChangesMultiPages(t *testing.T) { func TestReadChangesMultiPagesLimit(t *testing.T) { t.Parallel() - const layout = "2006-01-02T15:04:05Z" - mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) @@ -321,7 +315,7 @@ func TestReadChangesMultiPagesLimit(t *testing.T) { mockBody := mock_client.NewMockSdkClientReadChangesRequestInterface(mockCtrl) - sTime, err := time.Parse(layout, "2022-01-01T00:00:00Z") + sTime, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z") if err != nil { t.Error(err) } diff --git a/go.mod b/go.mod index ed835ea9..405dfbfc 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/nwidger/jsoncolor v0.3.2 github.com/oklog/ulid/v2 v2.1.0 github.com/openfga/api/proto v0.0.0-20250107154247-c22e6db5c4f5 - github.com/openfga/go-sdk v0.6.5-0.20250205144545-22df3ecba077 + github.com/openfga/go-sdk v0.6.5 github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c github.com/openfga/openfga v1.8.4 github.com/spf13/cobra v1.8.1 diff --git a/go.sum b/go.sum index 6abc15f0..59a83036 100644 --- a/go.sum +++ b/go.sum @@ -186,6 +186,8 @@ github.com/openfga/go-sdk v0.6.4 h1:VAeH9exZuG1qaBt41+mjIK5RxTtuL9maS4d47YsYGZw= github.com/openfga/go-sdk v0.6.4/go.mod h1:zui7pHE3eLAYh2fFmEMrWg9XbxYns2WW5Xr/GEgili4= github.com/openfga/go-sdk v0.6.5-0.20250205144545-22df3ecba077 h1:o1s2Y5cjg/ALF9vN2BiTcF6ucrGDGfJKR7N8QdjKcR4= github.com/openfga/go-sdk v0.6.5-0.20250205144545-22df3ecba077/go.mod h1:zui7pHE3eLAYh2fFmEMrWg9XbxYns2WW5Xr/GEgili4= +github.com/openfga/go-sdk v0.6.5 h1:2bxZkoLyphOahFETo9wPvls1AQ3IqbsygIyDkzRvx1k= +github.com/openfga/go-sdk v0.6.5/go.mod h1:zui7pHE3eLAYh2fFmEMrWg9XbxYns2WW5Xr/GEgili4= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c h1:1y84C0V4NRfPtRi4MqQ7+gnFtYgeBKPIeIAPLdVJ7j4= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c/go.mod h1:12RMe/HuRNyOzS33RQa53jwdcxE2znr8ycXMlVbgQN4= github.com/openfga/openfga v1.8.4 h1:OqyRpuxMCxcS7irTFYFkhAIYzmAnczNwxUqjnuZOQyo=