diff --git a/cmd/dry_run.go b/cmd/dry_run.go new file mode 100644 index 0000000..15fe28b --- /dev/null +++ b/cmd/dry_run.go @@ -0,0 +1,53 @@ +// Copyright © 2026 Dropbox, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" +) + +const ( + dryRunFlagName = "dry-run" + jsonStatusPlanned = "planned" +) + +// Dry-run support is limited here to shared flags and output conventions. +// Command implementations should keep their validate/plan/execute flow local +// until at least two commands need the same execution driver. +func addDryRunFlag(cmd *cobra.Command) { + cmd.Flags().Bool(dryRunFlagName, false, "Show what would be done without making changes") +} + +func dryRunEnabled(cmd *cobra.Command) (bool, error) { + return cmd.Flags().GetBool(dryRunFlagName) +} + +// Dry-run JSON results use status "planned" and include dry_run=true in the +// result input. They do not preserve the real mutation status, because no +// mutation happened. +func plannedStatus(dryRun bool, realStatus string) string { + if dryRun { + return jsonStatusPlanned + } + return realStatus +} + +func writeDryRunLine(w io.Writer, verb, path string) error { + _, err := fmt.Fprintf(w, "Would %s %s\n", verb, path) + return err +} diff --git a/cmd/dry_run_test.go b/cmd/dry_run_test.go new file mode 100644 index 0000000..f75d3a8 --- /dev/null +++ b/cmd/dry_run_test.go @@ -0,0 +1,81 @@ +// Copyright © 2026 Dropbox, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "bytes" + "testing" + + "github.com/spf13/cobra" +) + +func TestAddDryRunFlag(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + + addDryRunFlag(cmd) + + flag := cmd.Flags().Lookup(dryRunFlagName) + if flag == nil { + t.Fatalf("%q flag not registered", dryRunFlagName) + } + if flag.Shorthand != "" { + t.Fatalf("dry-run shorthand = %q, want none", flag.Shorthand) + } + enabled, err := dryRunEnabled(cmd) + if err != nil { + t.Fatalf("dryRunEnabled error: %v", err) + } + if enabled { + t.Fatal("dry-run default = true, want false") + } + + if err := cmd.Flags().Set(dryRunFlagName, "true"); err != nil { + t.Fatal(err) + } + enabled, err = dryRunEnabled(cmd) + if err != nil { + t.Fatalf("dryRunEnabled error after set: %v", err) + } + if !enabled { + t.Fatal("dry-run = false after setting flag") + } +} + +func TestPlannedStatus(t *testing.T) { + if got, want := plannedStatus(false, "deleted"), "deleted"; got != want { + t.Fatalf("plannedStatus(false) = %q, want %q", got, want) + } + if got, want := plannedStatus(true, "deleted"), jsonStatusPlanned; got != want { + t.Fatalf("plannedStatus(true) = %q, want %q", got, want) + } +} + +func TestWriteDryRunLine(t *testing.T) { + var stdout bytes.Buffer + if err := writeDryRunLine(&stdout, "delete", "/File.txt"); err != nil { + t.Fatalf("writeDryRunLine error: %v", err) + } + if got, want := stdout.String(), "Would delete /File.txt\n"; got != want { + t.Fatalf("writeDryRunLine output = %q, want %q", got, want) + } + + stdout.Reset() + if err := writeDryRunLine(&stdout, "permanently delete", "/File.txt"); err != nil { + t.Fatalf("writeDryRunLine permanent error: %v", err) + } + if got, want := stdout.String(), "Would permanently delete /File.txt\n"; got != want { + t.Fatalf("writeDryRunLine permanent output = %q, want %q", got, want) + } +} diff --git a/cmd/help_manifest.go b/cmd/help_manifest.go index dc7bf8e..fee2f7e 100644 --- a/cmd/help_manifest.go +++ b/cmd/help_manifest.go @@ -218,9 +218,10 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{ Args: []jsonCommandArg{commandArg("file", true, true, "dropbox_path", "Dropbox path to remove")}, Examples: []jsonCommandExample{{Description: "Remove a Dropbox path", Command: "dbxcli rm /old.txt"}}, Flags: map[string]jsonCommandFlagMetadata{ - "force": {ValueKind: "boolean"}, - "permanent": {ValueKind: "boolean"}, - "recursive": {ValueKind: "boolean"}, + dryRunFlagName: {ValueKind: "boolean"}, + "force": {ValueKind: "boolean"}, + "permanent": {ValueKind: "boolean"}, + "recursive": {ValueKind: "boolean"}, }, DropboxScopes: []string{"files.content.write", "files.metadata.read"}, Known: true, @@ -352,7 +353,7 @@ var commandContractRegistry = map[string]jsonCommandContractMetadata{ "put": {Statuses: []string{"autorenamed", "created", "existing", "skipped", "uploaded"}, Kinds: []string{"file", "folder"}, Warnings: []string{jsonWarningCodeSkippedSymlink}}, "restore": {Statuses: []string{"restored"}, Kinds: []string{"file"}}, "revs": {Statuses: []string{"revision"}, Kinds: []string{"file"}}, - "rm": {Statuses: []string{"deleted", "permanently_deleted"}, Kinds: []string{"deleted", "file", "folder"}}, + "rm": {Statuses: []string{"deleted", "permanently_deleted", jsonStatusPlanned}, Kinds: []string{"deleted", "file", "folder"}}, "search": {Statuses: []string{"found"}, Kinds: []string{"deleted", "file", "folder"}}, "share list folder": {Statuses: []string{"listed"}, Kinds: []string{"shared_folder"}}, "share list link": {Statuses: []string{"listed"}, Kinds: []string{"file", "folder", "link"}, Warnings: []string{jsonWarningCodeDeprecatedCommand}}, diff --git a/cmd/json_contract_test.go b/cmd/json_contract_test.go index 94b4ca0..95b3e06 100644 --- a/cmd/json_contract_test.go +++ b/cmd/json_contract_test.go @@ -1097,7 +1097,7 @@ func jsonCommandSchemas() map[string]jsonGoldenCommandSchema { "put": operationSchema("put_input", schemaRef("put_result_input"), "metadata", []string{putStatusAutorenamed, putStatusCreated, putStatusExisting, putStatusSkipped, putStatusUploaded}, []string{putKindFile, putKindFolder}, []string{jsonWarningCodeSkippedSymlink}), "restore": operationSchema("restore_input", schemaRef("restore_input"), "metadata", []string{restoreStatusRestored}, []string{restoreKindFile}, nil), "revs": operationSchema("revs_input", schemaRef("empty"), "metadata", []string{revsJSONStatusRevision}, []string{"file"}, nil), - "rm": operationSchema("empty", schemaRef("remove_input"), "metadata", []string{removeJSONStatusDeleted, removeJSONStatusPermanentlyDeleted}, metadataKinds(), nil), + "rm": operationSchema("empty", schemaRef("remove_input"), "metadata", []string{removeJSONStatusDeleted, removeJSONStatusPermanentlyDeleted, jsonStatusPlanned}, metadataKinds(), nil), "search": operationSchema("search_input", schemaRef("empty"), "metadata", []string{searchJSONStatusFound}, metadataKinds(), nil), "share list folder": operationSchema("empty", schemaRef("empty"), "share_folder", []string{shareFolderJSONStatusListed}, []string{shareFolderJSONKindFolder}, nil), "share list link": operationSchema("share_link_list_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusListed}, shareLinkKinds(), []string{jsonWarningCodeDeprecatedCommand}), diff --git a/cmd/rm.go b/cmd/rm.go index 119c633..d7172f3 100644 --- a/cmd/rm.go +++ b/cmd/rm.go @@ -27,6 +27,7 @@ type removeOptions struct { force bool recursive bool permanent bool + dryRun bool verbose bool } @@ -40,6 +41,7 @@ type removeInput struct { Permanent bool `json:"permanent"` Recursive bool `json:"recursive"` Force bool `json:"force"` + DryRun bool `json:"dry_run,omitempty"` } type removeResult struct { @@ -75,7 +77,7 @@ func rm(cmd *cobra.Command, args []string) error { } return commandOutput(cmd).Render(func(w io.Writer) error { - if !opts.verbose { + if !opts.dryRun && !opts.verbose { return nil } return renderRemoveResults(w, results) @@ -92,9 +94,9 @@ func removeOperationResults(results []removeResult) []jsonOperationResult { func removeJSONStatus(result removeResult) string { if result.Input.Permanent { - return removeJSONStatusPermanentlyDeleted + return plannedStatus(result.Input.DryRun, removeJSONStatusPermanentlyDeleted) } - return removeJSONStatusDeleted + return plannedStatus(result.Input.DryRun, removeJSONStatusDeleted) } func parseRemoveOptions(cmd *cobra.Command) (removeOptions, error) { @@ -113,12 +115,18 @@ func parseRemoveOptions(cmd *cobra.Command) (removeOptions, error) { return removeOptions{}, err } + dryRun, err := dryRunEnabled(cmd) + if err != nil { + return removeOptions{}, err + } + verbose, _ := cmd.Flags().GetBool("verbose") return removeOptions{ force: force, recursive: recursive, permanent: permanent, + dryRun: dryRun, verbose: verbose, }, nil } @@ -161,17 +169,19 @@ func removeTargets(dbx filesClient, targets []removeTarget, opts removeOptions) arg := files.NewDeleteArg(target.path) metadata := target.metadata - if opts.permanent { - if err := dbx.PermanentlyDeleteContext(currentContext(), arg); err != nil { - return nil, withJSONErrorDetails(err, operationErrorDetails(removeOperation(opts)), pathErrorDetails(target.path)) - } - } else { - res, err := dbx.DeleteV2Context(currentContext(), arg) - if err != nil { - return nil, withJSONErrorDetails(err, operationErrorDetails(removeOperation(opts)), pathErrorDetails(target.path)) - } - if res != nil && res.Metadata != nil { - metadata = res.Metadata + if !opts.dryRun { + if opts.permanent { + if err := dbx.PermanentlyDeleteContext(currentContext(), arg); err != nil { + return nil, withJSONErrorDetails(err, operationErrorDetails(removeOperation(opts)), pathErrorDetails(target.path)) + } + } else { + res, err := dbx.DeleteV2Context(currentContext(), arg) + if err != nil { + return nil, withJSONErrorDetails(err, operationErrorDetails(removeOperation(opts)), pathErrorDetails(target.path)) + } + if res != nil && res.Metadata != nil { + metadata = res.Metadata + } } } @@ -196,6 +206,7 @@ func newRemoveResult(path string, metadata files.IsMetadata, opts removeOptions) Permanent: opts.permanent, Recursive: opts.recursive, Force: opts.force, + DryRun: opts.dryRun, }, Result: result, }, nil @@ -212,6 +223,18 @@ func removeMetadataFromDropbox(path string, metadata files.IsMetadata) (jsonMeta func renderRemoveResults(w io.Writer, results []removeResult) error { for _, result := range results { + if result.Input.DryRun { + if result.Input.Permanent { + if err := writeDryRunLine(w, "permanently delete", result.displayPath()); err != nil { + return err + } + continue + } + if err := writeDryRunLine(w, "delete", result.displayPath()); err != nil { + return err + } + continue + } if result.Input.Permanent { if _, err := fmt.Fprintf(w, "Permanently deleted %s\n", result.displayPath()); err != nil { return err @@ -257,4 +280,5 @@ func init() { rmCmd.Flags().BoolP("force", "f", false, "Allow removing non-empty folders; same as --recursive") rmCmd.Flags().BoolP("recursive", "r", false, "Recursively remove folders") rmCmd.Flags().Bool("permanent", false, "Permanently delete instead of moving to Dropbox trash") + addDryRunFlag(rmCmd) } diff --git a/cmd/rm_test.go b/cmd/rm_test.go index 754ba82..5be0ded 100644 --- a/cmd/rm_test.go +++ b/cmd/rm_test.go @@ -21,6 +21,7 @@ func testRmCmd(t *testing.T) (*cobra.Command, *bytes.Buffer) { cmd.Flags().BoolP("force", "f", false, "") cmd.Flags().BoolP("recursive", "r", false, "") cmd.Flags().Bool("permanent", false, "") + addDryRunFlag(cmd) cmd.Flags().BoolP("verbose", "v", false, "") cmd.Flags().String(outputFlag, "text", "") return cmd, &stdout @@ -72,6 +73,17 @@ type removeOutput struct { Warnings []jsonWarning `json:"warnings"` } +type removeOperationOutput struct { + Input map[string]any `json:"input"` + Results []struct { + Status string `json:"status"` + Kind string `json:"kind"` + Input removeInput `json:"input"` + Result jsonMetadata `json:"result"` + } `json:"results"` + Warnings []jsonWarning `json:"warnings"` +} + func decodeRemoveOutput(t *testing.T, stdout *bytes.Buffer) removeOutput { t.Helper() @@ -288,6 +300,98 @@ func TestRmPermanentCallsPermanentlyDelete(t *testing.T) { } } +func TestRmDryRunTextOutputSnapshot(t *testing.T) { + cmd, stdout := testRmCmd(t) + setRmFlag(t, cmd, dryRunFlagName) + file := rmFileMetadata("/File.txt") + + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + return file, nil + }, + listFolderFn: func(arg *files.ListFolderArg) (*files.ListFolderResult, error) { + t.Fatalf("ListFolder called for file: %v", arg) + return nil, nil + }, + deleteV2Fn: func(arg *files.DeleteArg) (*files.DeleteResult, error) { + t.Fatalf("DeleteV2 called for dry-run: %v", arg) + return nil, nil + }, + permanentlyDeleteFn: func(arg *files.DeleteArg) error { + t.Fatalf("PermanentlyDelete called for dry-run: %v", arg) + return nil + }, + } + stubFilesClient(t, mock) + + if err := rm(cmd, []string{"/file.txt"}); err != nil { + t.Fatalf("rm error: %v", err) + } + if got, want := stdout.String(), "Would delete /File.txt\n"; got != want { + t.Fatalf("stdout = %q, want %q", got, want) + } +} + +func TestRmDryRunPermanentPrintsPlanWithoutDeleting(t *testing.T) { + cmd, stdout := testRmCmd(t) + setRmFlag(t, cmd, dryRunFlagName) + setRmFlag(t, cmd, "permanent") + file := rmFileMetadata("/File.txt") + + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + return file, nil + }, + deleteV2Fn: func(arg *files.DeleteArg) (*files.DeleteResult, error) { + t.Fatalf("DeleteV2 called for dry-run permanent delete: %v", arg) + return nil, nil + }, + permanentlyDeleteFn: func(arg *files.DeleteArg) error { + t.Fatalf("PermanentlyDelete called for dry-run: %v", arg) + return nil + }, + } + stubFilesClient(t, mock) + + if err := rm(cmd, []string{"/file.txt"}); err != nil { + t.Fatalf("rm error: %v", err) + } + if got, want := stdout.String(), "Would permanently delete /File.txt\n"; got != want { + t.Fatalf("stdout = %q, want %q", got, want) + } +} + +func TestRmDryRunStillValidatesNonEmptyFolder(t *testing.T) { + cmd, _ := testRmCmd(t) + setRmFlag(t, cmd, dryRunFlagName) + deleteCalled := false + + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + return rmFolderMetadata("/folder"), nil + }, + listFolderFn: func(arg *files.ListFolderArg) (*files.ListFolderResult, error) { + return rmNonEmptyFolderResult(), nil + }, + deleteV2Fn: func(arg *files.DeleteArg) (*files.DeleteResult, error) { + deleteCalled = true + return nil, nil + }, + } + stubFilesClient(t, mock) + + err := rm(cmd, []string{"/folder"}) + if err == nil { + t.Fatal("expected validation error") + } + if !strings.Contains(err.Error(), "--recursive") || !strings.Contains(err.Error(), "--force") { + t.Fatalf("error = %q, want recursive and force guidance", err.Error()) + } + if deleteCalled { + t.Fatal("delete called after dry-run validation failure") + } +} + func TestRmPermanentRecursiveDeletesNonEmptyFolder(t *testing.T) { cmd, _ := testRmCmd(t) setRmFlag(t, cmd, "permanent") @@ -391,6 +495,7 @@ func TestRmVerboseUsesInheritedFlag(t *testing.T) { cmd.Flags().BoolP("force", "f", false, "") cmd.Flags().BoolP("recursive", "r", false, "") cmd.Flags().Bool("permanent", false, "") + addDryRunFlag(cmd) root.AddCommand(cmd) root.SetArgs([]string{"rm", "--verbose", "/file.txt"}) @@ -569,6 +674,87 @@ func TestRmJSONPermanentUsesValidatedMetadata(t *testing.T) { } } +func TestRmJSONDryRunOutputsPlannedResult(t *testing.T) { + cmd, stdout := testRmCmd(t) + setRmOutputJSON(t, cmd) + setRmFlag(t, cmd, dryRunFlagName) + file := rmFileMetadata("/File.txt") + + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + return file, nil + }, + deleteV2Fn: func(arg *files.DeleteArg) (*files.DeleteResult, error) { + t.Fatalf("DeleteV2 called for dry-run: %v", arg) + return nil, nil + }, + permanentlyDeleteFn: func(arg *files.DeleteArg) error { + t.Fatalf("PermanentlyDelete called for dry-run: %v", arg) + return nil + }, + } + stubFilesClient(t, mock) + + if err := rm(cmd, []string{"/File.txt"}); err != nil { + t.Fatalf("rm error: %v", err) + } + + var got removeOperationOutput + if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + t.Fatalf("decode JSON output: %v\noutput: %s", err, stdout.String()) + } + if len(got.Results) != 1 { + t.Fatalf("results len = %d, want 1", len(got.Results)) + } + result := got.Results[0] + if result.Status != jsonStatusPlanned { + t.Fatalf("status = %q, want %q", result.Status, jsonStatusPlanned) + } + if result.Kind != "file" { + t.Fatalf("kind = %q, want file", result.Kind) + } + if result.Input.Path != "/File.txt" || !result.Input.DryRun { + t.Fatalf("input = %+v, want path /File.txt and dry_run true", result.Input) + } + if result.Input.Permanent || result.Input.Recursive || result.Input.Force { + t.Fatalf("input flags = %+v, want only dry_run true", result.Input) + } + if result.Result.PathDisplay != "/File.txt" { + t.Fatalf("path_display = %q, want /File.txt", result.Result.PathDisplay) + } +} + +func TestRmJSONDryRunOutputSnapshot(t *testing.T) { + cmd, stdout := testRmCmd(t) + setRmOutputJSON(t, cmd) + setRmFlag(t, cmd, dryRunFlagName) + file := rmFileMetadata("/File.txt") + + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + return file, nil + }, + deleteV2Fn: func(arg *files.DeleteArg) (*files.DeleteResult, error) { + t.Fatalf("DeleteV2 called for dry-run: %v", arg) + return nil, nil + }, + permanentlyDeleteFn: func(arg *files.DeleteArg) error { + t.Fatalf("PermanentlyDelete called for dry-run: %v", arg) + return nil + }, + } + stubFilesClient(t, mock) + + if err := rm(cmd, []string{"/File.txt"}); err != nil { + t.Fatalf("rm error: %v", err) + } + + want := `{"ok":true,"schema_version":"1","command":"rm","input":{},"results":[{"status":"planned","kind":"file","input":{"path":"/File.txt","permanent":false,"recursive":false,"force":false,"dry_run":true},"result":{"type":"file","path_display":"/File.txt","path_lower":"/file.txt","id":"id:file","rev":"rev","size":123}}],"warnings":[]}` + "\n" + if got := stdout.String(); got != want { + t.Fatalf("stdout = %q, want %q", got, want) + } +} + func TestRmJSONMultipleTargets(t *testing.T) { cmd, stdout := testRmCmd(t) setRmOutputJSON(t, cmd) diff --git a/cmd/testdata/json_contract/success_schemas.json b/cmd/testdata/json_contract/success_schemas.json index cf672d8..0e873fd 100644 --- a/cmd/testdata/json_contract/success_schemas.json +++ b/cmd/testdata/json_contract/success_schemas.json @@ -213,6 +213,7 @@ "to_path" ], "remove_input": [ + "dry_run", "force", "path", "permanent", @@ -595,7 +596,8 @@ "result": "metadata", "statuses": [ "deleted", - "permanently_deleted" + "permanently_deleted", + "planned" ], "kinds": [ "deleted", diff --git a/docs/commands/dbxcli_rm.md b/docs/commands/dbxcli_rm.md index ef989d0..acdf2b1 100644 --- a/docs/commands/dbxcli_rm.md +++ b/docs/commands/dbxcli_rm.md @@ -11,6 +11,7 @@ dbxcli rm [flags] ### Options ``` + --dry-run Show what would be done without making changes -f, --force Allow removing non-empty folders; same as --recursive -h, --help help for rm --permanent Permanently delete instead of moving to Dropbox trash @@ -36,7 +37,7 @@ dbxcli rm [flags] * Arguments: `file` (required, dropbox_path, variadic) * Flag metadata: `--output` (values: `json`, `text`) * Destructive behavior: `delete` -* Result statuses: `deleted`, `permanently_deleted` +* Result statuses: `deleted`, `permanently_deleted`, `planned` * Result kinds: `deleted`, `file`, `folder` * JSON contract: `docs/json-schema/v1/commands.json#/commands/rm` * JSON success schema: `docs/json-schema/v1/commands.schema.json#/$defs/command_rm` diff --git a/docs/json-schema/v1/commands.json b/docs/json-schema/v1/commands.json index cf672d8..0e873fd 100644 --- a/docs/json-schema/v1/commands.json +++ b/docs/json-schema/v1/commands.json @@ -213,6 +213,7 @@ "to_path" ], "remove_input": [ + "dry_run", "force", "path", "permanent", @@ -595,7 +596,8 @@ "result": "metadata", "statuses": [ "deleted", - "permanently_deleted" + "permanently_deleted", + "planned" ], "kinds": [ "deleted", diff --git a/docs/json-schema/v1/commands.schema.json b/docs/json-schema/v1/commands.schema.json index d5230d6..4cd8429 100644 --- a/docs/json-schema/v1/commands.schema.json +++ b/docs/json-schema/v1/commands.schema.json @@ -1878,6 +1878,9 @@ "remove_input": { "additionalProperties": false, "properties": { + "dry_run": { + "type": "boolean" + }, "force": { "type": "boolean" }, @@ -2290,7 +2293,8 @@ "status": { "enum": [ "deleted", - "permanently_deleted" + "permanently_deleted", + "planned" ] } }, diff --git a/internal/jsonschema/definition_schemas.go b/internal/jsonschema/definition_schemas.go index 46284cb..5c15d33 100644 --- a/internal/jsonschema/definition_schemas.go +++ b/internal/jsonschema/definition_schemas.go @@ -346,7 +346,7 @@ func defaultPropertySchema(field string) map[string]any { return stringArraySchema() case "allocated", "limit", "member_count", "num_licensed_users", "num_provisioned_users", "size", "used", "user_within_team_space_allocated", "user_within_team_space_used_cached": return integerSchema() - case "additionalProperties", "allow_comments", "allow_download", "can_allow_download", "can_disallow_download", "can_remove_expiry", "can_remove_password", "can_revoke", "can_set_expiry", "can_set_password", "can_use_extended_sharing_controls", "content", "deleted", "direct_only", "disabled", "disallow_download", "email_verified", "force", "help", "include_deleted", "inherited", "is_directory_restricted", "is_inside_team_folder", "is_paired", "is_team_folder", "is_teammate", "long", "may_prompt", "only_deleted", "parents", "password", "permanent", "recursive", "refreshable", "remote_token_revoked", "remove_expiration", "remove_password", "removed_saved_credentials", "require_password", "reverse", "runnable", "sensitive", "stdin", "stdout", "stream_dash", "supports_structured_output", "variadic", "writeOnly", "writes_binary_stdout", "x-inherited", "x-may-prompt", "x-sensitive", "x-stream-dash": + case "additionalProperties", "allow_comments", "allow_download", "can_allow_download", "can_disallow_download", "can_remove_expiry", "can_remove_password", "can_revoke", "can_set_expiry", "can_set_password", "can_use_extended_sharing_controls", "content", "deleted", "direct_only", "disabled", "disallow_download", "dry_run", "email_verified", "force", "help", "include_deleted", "inherited", "is_directory_restricted", "is_inside_team_folder", "is_paired", "is_team_folder", "is_teammate", "long", "may_prompt", "only_deleted", "parents", "password", "permanent", "recursive", "refreshable", "remote_token_revoked", "remove_expiration", "remove_password", "removed_saved_credentials", "require_password", "reverse", "runnable", "sensitive", "stdin", "stdout", "stream_dash", "supports_structured_output", "variadic", "writeOnly", "writes_binary_stdout", "x-inherited", "x-may-prompt", "x-sensitive", "x-stream-dash": return booleanSchema() case "client_modified", "expires", "invited_on", "joined_on", "server_modified", "suspended_on", "time_invited": return dateTimeStringSchema()