diff --git a/README.md b/README.md index f2657bc0..52ca2b69 100644 --- a/README.md +++ b/README.md @@ -1309,15 +1309,17 @@ fga mapping **init** [mapping.yaml] Created mapping.yaml ``` -##### Run Mapping +##### Evaluate Mapping ###### Command -fga mapping **run** [mapping-file] +fga mapping **evaluate** [mapping-file] Reads JSONL (one JSON object per line) from stdin (or `--input`) and emits tuple operations as JSONL (default) or a JSON batch. Runs entirely offline. Rules using `tuple_filters` cannot be expanded without a store; they are reported as warnings on stderr, or under `tuple_filter_operations` with `--format json`. The mapping file is optional: omit it in an interactive terminal to choose one from a `.yaml`/`.yml` file picker. +> `fga mapping run` is a deprecated alias for `fga mapping evaluate` and will be removed in a future release. + ###### Parameters * `--input`: Path to a JSONL input file, one JSON object per line (default: stdin) * `--format`: Output format — `jsonl` (default) or `json` @@ -1327,9 +1329,9 @@ The mapping file is optional: omit it in an interactive terminal to choose one f * `--interactive` / `-i`: Explore the mapping in a terminal loop — type or paste a JSON document and see the tuple operations it produces. The document is evaluated as soon as it forms a complete JSON value, so a single-line object is evaluated on Enter and a multi-line one when its closing brace is typed. Supports line editing (arrow keys, history). Requires an interactive terminal (both stdin and stdout must be a TTY) and cannot be combined with `--input`, `--writes-only`, `--format`, `--aggregate`, or `--continue-on-error`. ###### Example -`echo '{"id":"anne","org":"acme"}' | fga mapping run mapping.yaml` +`echo '{"id":"anne","org":"acme"}' | fga mapping evaluate mapping.yaml` -`fga mapping run --writes-only mapping.yaml --input event.json > out.jsonl && fga tuple write --store-id $FGA_STORE_ID --file out.jsonl` +`fga mapping evaluate --writes-only mapping.yaml --input event.json > out.jsonl && fga tuple write --store-id $FGA_STORE_ID --file out.jsonl` ###### Response ```json @@ -1340,7 +1342,7 @@ The mapping file is optional: omit it in an interactive terminal to choose one f In a terminal, `-i` starts an explorer loop. Type or paste a JSON document and the resulting tuple operations are printed as an aligned table. The document is evaluated as soon as it parses as complete JSON — a single-line object on Enter, a multi-line one when its closing brace is typed. While more input is expected the prompt shows `...` and a one-time hint notes that the document is not yet valid JSON; pressing Enter on a blank line evaluates whatever is buffered. Invalid JSON is reported with the line, column, and a caret under the offending character. A rule with `tuple_filters` cannot be resolved offline, so its filter conditions are shown as `filter:patch` or `filter:delete` rows (the action distinguishes how the store is reconciled), with the desired-state tuples it reconciles toward shown as indented `desired` rows (they drive a read-diff-write against a store rather than being written directly). Filter fields left unset match any value and render as `*`, and a conditioned tuple shows its condition name and rendered context in brackets. Line editing (arrow keys, history) is available. Available commands: `:reload` re-reads and recompiles the mapping from disk, `:trace on|off` toggles the per-rule trace, and `:quit` exits. ``` -$ fga mapping run mapping.yaml -i +$ fga mapping evaluate mapping.yaml -i mapping loaded: 2 rules. Type or paste a JSON document; it is evaluated once complete. commands: :reload :trace on|off :quit > {"id":"anne","org":"acme"} diff --git a/cmd/mapping/mapping.go b/cmd/mapping/mapping.go index a40b5170..2637ec35 100644 --- a/cmd/mapping/mapping.go +++ b/cmd/mapping/mapping.go @@ -74,5 +74,5 @@ func init() { MappingCmd.AddCommand(validateCmd) MappingCmd.AddCommand(testCmd) MappingCmd.AddCommand(initCmd) - MappingCmd.AddCommand(runCmd) + MappingCmd.AddCommand(evaluateCmd) } diff --git a/cmd/mapping/run.go b/cmd/mapping/run.go index 6c5dd7c1..0bf8efd1 100644 --- a/cmd/mapping/run.go +++ b/cmd/mapping/run.go @@ -466,17 +466,18 @@ func emitBuffered( } var ( - runFormat string - runWritesOnly bool - runInputFile string - runAggregate bool - runContinueOnError bool - runInteractive bool + evaluateFormat string + evaluateWritesOnly bool + evaluateInputFile string + evaluateAggregate bool + evaluateContinueOnError bool + evaluateInteractive bool ) -var runCmd = &cobra.Command{ - Use: "run [mapping-file]", - Short: "Evaluate a mapping against JSON input and emit tuple operations", +var evaluateCmd = &cobra.Command{ + Use: "evaluate [mapping-file]", + Aliases: []string{"run"}, + Short: "Evaluate a mapping against JSON input and emit tuple operations", Long: `Reads JSONL from stdin (or --input) and evaluates it against the mapping file. Input is JSON Lines: one JSON object per line. Outputs tuple operations as JSONL (default) or a JSON batch (--format json). @@ -495,26 +496,30 @@ a conflict is a runtime error. Both --format json and --aggregate buffer the who before emitting; the default streaming JSONL does not. --continue-on-error skips any record that fails to parse or evaluate (warning to stderr) and continues; the command still exits non-zero if any record was skipped.`, - Example: ` echo '{"id":"anne","org":"acme"}' | fga mapping run mapping.yaml - fga mapping run mapping.yaml --input event.json --format json - fga mapping run --writes-only mapping.yaml > out.jsonl && fga tuple write --store-id $STORE_ID --file out.jsonl - fga mapping run mapping.yaml -i`, + Example: ` echo '{"id":"anne","org":"acme"}' | fga mapping evaluate mapping.yaml + fga mapping evaluate mapping.yaml --input event.json --format json + fga mapping evaluate --writes-only mapping.yaml > out.jsonl && fga tuple write --store-id $STORE_ID --file out.jsonl + fga mapping evaluate mapping.yaml -i`, Args: cobra.RangeArgs(0, 1), RunE: func(cmd *cobra.Command, args []string) error { + if cmd.CalledAs() == "run" { + fmt.Fprintln(cmd.ErrOrStderr(), `warning: "fga mapping run" is deprecated, use "fga mapping evaluate" instead`) + } + errStream := cmd.ErrOrStderr() opts := runMappingOptions{ - format: runFormat, - writesOnly: runWritesOnly, - aggregate: runAggregate, - continueOnError: runContinueOnError, + format: evaluateFormat, + writesOnly: evaluateWritesOnly, + aggregate: evaluateAggregate, + continueOnError: evaluateContinueOnError, } // Validate the interactive invocation before any prompt, so a bad flag // combination or a redirected stream never blocks on asking for a mapping // path first. - if runInteractive { - if err := checkInteractiveFlags(opts, runInputFile, cmd.Flags().Changed("format")); err != nil { + if evaluateInteractive { + if err := checkInteractiveFlags(opts, evaluateInputFile, cmd.Flags().Changed("format")); err != nil { fmt.Fprintln(errStream, "Error: "+err.Error()) os.Exit(2) } @@ -530,7 +535,7 @@ continues; the command still exits non-zero if any record was skipped.`, path := promptMappingFile(args, errStream) - if runInteractive { + if evaluateInteractive { err := runMappingInteractive(cmd.Context(), path, cmd.InOrStdin(), cmd.OutOrStdout(), errStream) if errors.Is(err, errMappingInvalid) { os.Exit(2) @@ -541,10 +546,10 @@ continues; the command still exits non-zero if any record was skipped.`, inputReader := cmd.InOrStdin() - if runInputFile != "" { - file, err := os.Open(runInputFile) + if evaluateInputFile != "" { + file, err := os.Open(evaluateInputFile) if err != nil { - return fmt.Errorf("opening input %s: %w", runInputFile, err) + return fmt.Errorf("opening input %s: %w", evaluateInputFile, err) } defer file.Close() @@ -570,23 +575,23 @@ continues; the command still exits non-zero if any record was skipped.`, } func init() { - runCmd.Flags().StringVar(&runFormat, "format", "jsonl", `Output format: "jsonl" or "json"`) - runCmd.Flags().BoolVar( - &runWritesOnly, "writes-only", false, + evaluateCmd.Flags().StringVar(&evaluateFormat, "format", "jsonl", `Output format: "jsonl" or "json"`) + evaluateCmd.Flags().BoolVar( + &evaluateWritesOnly, "writes-only", false, "Emit only write operations in ClientTupleKey format (consumable by fga tuple write)", ) - runCmd.Flags().StringVar( - &runInputFile, "input", "", "Path to JSONL input file, one JSON object per line (default: stdin)") - runCmd.Flags().BoolVar( - &runAggregate, "aggregate", false, + evaluateCmd.Flags().StringVar( + &evaluateInputFile, "input", "", "Path to JSONL input file, one JSON object per line (default: stdin)") + evaluateCmd.Flags().BoolVar( + &evaluateAggregate, "aggregate", false, "Buffer all records and collapse them (dedup tuples and filters, detect write/delete conflicts) before emitting", ) - runCmd.Flags().BoolVar( - &runContinueOnError, "continue-on-error", false, + evaluateCmd.Flags().BoolVar( + &evaluateContinueOnError, "continue-on-error", false, "Skip input records that fail to parse or evaluate (warn to stderr) and exit non-zero if any were skipped", ) - runCmd.Flags().BoolVarP( - &runInteractive, "interactive", "i", false, + evaluateCmd.Flags().BoolVarP( + &evaluateInteractive, "interactive", "i", false, "Explore the mapping in a terminal loop: paste JSON documents and see the tuples they produce (requires a TTY)", ) } diff --git a/tests/mapping-test-cases.yaml b/tests/mapping-test-cases.yaml index 4b3e61c4..c8f7cc5a 100644 --- a/tests/mapping-test-cases.yaml +++ b/tests/mapping-test-cases.yaml @@ -80,9 +80,9 @@ tests: not-contains: - "tests:" - # fga mapping run - 012 - run emits JSONL tuple from stdin: - command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run ./tests/fixtures/basic-mapping.yaml' + # fga mapping evaluate (run is a deprecated alias) + 012 - evaluate emits JSONL tuple from stdin: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping evaluate ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: json: @@ -91,8 +91,8 @@ tests: relation: "member" object: "org:acme" - 013 - run --writes-only emits ClientTupleKey shape without op or action field: - command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run --writes-only ./tests/fixtures/basic-mapping.yaml' + 013 - evaluate --writes-only emits ClientTupleKey shape without op or action field: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping evaluate --writes-only ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: json: @@ -103,41 +103,41 @@ tests: - '"op"' - '"action"' - 014 - run --input reads from file: - command: fga mapping run --input ./tests/fixtures/mapping-event.json ./tests/fixtures/basic-mapping.yaml + 014 - evaluate --input reads from file: + command: fga mapping evaluate --input ./tests/fixtures/mapping-event.json ./tests/fixtures/basic-mapping.yaml exit-code: 0 stdout: json: op: "write" user: "user:anne" - 015 - run --format json --writes-only returns a flat array of tuples: - command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run --format json --writes-only ./tests/fixtures/basic-mapping.yaml' + 015 - evaluate --format json --writes-only returns a flat array of tuples: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping evaluate --format json --writes-only ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: json: 0.user: "user:anne" - 016 - run --format json returns a batch object with writes: - command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run --format json ./tests/fixtures/basic-mapping.yaml' + 016 - evaluate --format json returns a batch object with writes: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping evaluate --format json ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: json: writes.0.user: "user:anne" - 017 - run rejects unknown format: - command: bash -c 'echo '"'"'{}'"'"' | fga mapping run --format xml ./tests/fixtures/basic-mapping.yaml' + 017 - evaluate rejects unknown format: + command: bash -c 'echo '"'"'{}'"'"' | fga mapping evaluate --format xml ./tests/fixtures/basic-mapping.yaml' exit-code: 2 - 018 - run rejects invalid mapping: - command: bash -c 'echo '"'"'{}'"'"' | fga mapping run ./tests/fixtures/invalid-mapping.yaml' + 018 - evaluate rejects invalid mapping: + command: bash -c 'echo '"'"'{}'"'"' | fga mapping evaluate ./tests/fixtures/invalid-mapping.yaml' exit-code: 2 stderr: contains: - "unclosed" - 019 - run streams one op per record for multi-record input: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping run ./tests/fixtures/basic-mapping.yaml' + 019 - evaluate streams one op per record for multi-record input: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping evaluate ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: line-count: 2 @@ -145,21 +145,21 @@ tests: - "user:anne" - "user:bob" - 020 - run --aggregate dedups an identical tuple across records: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"anne","org":"acme"}\n'"'"' | fga mapping run --aggregate ./tests/fixtures/basic-mapping.yaml' + 020 - evaluate --aggregate dedups an identical tuple across records: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"anne","org":"acme"}\n'"'"' | fga mapping evaluate --aggregate ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: line-count: 1 - 021 - run --aggregate reports a cross-record write/delete conflict: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme","op":"add"}\n{"id":"anne","org":"acme","op":"remove"}\n'"'"' | fga mapping run --aggregate ./tests/fixtures/conflicting-mapping.yaml' + 021 - evaluate --aggregate reports a cross-record write/delete conflict: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme","op":"add"}\n{"id":"anne","org":"acme","op":"remove"}\n'"'"' | fga mapping evaluate --aggregate ./tests/fixtures/conflicting-mapping.yaml' exit-code: 1 stderr: contains: - "conflict" - 022 - run --continue-on-error skips a malformed record and exits non-zero: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\nnot json\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping run --continue-on-error ./tests/fixtures/basic-mapping.yaml' + 022 - evaluate --continue-on-error skips a malformed record and exits non-zero: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\nnot json\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping evaluate --continue-on-error ./tests/fixtures/basic-mapping.yaml' exit-code: 1 stdout: line-count: 2 @@ -170,15 +170,28 @@ tests: contains: - "skipping record on line 2" - 023 - run --continue-on-error with all valid records exits zero: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping run --continue-on-error ./tests/fixtures/basic-mapping.yaml' + 023 - evaluate --continue-on-error with all valid records exits zero: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping evaluate --continue-on-error ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: line-count: 2 - 024 - run --aggregate dedups identical unresolved filters across records: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"anne","org":"acme"}\n'"'"' | fga mapping run --aggregate --format json ./tests/fixtures/filter-mapping.yaml' + 024 - evaluate --aggregate dedups identical unresolved filters across records: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"anne","org":"acme"}\n'"'"' | fga mapping evaluate --aggregate --format json ./tests/fixtures/filter-mapping.yaml' exit-code: 0 stdout: json: tuple_filter_operations.#: "1" + + 025 - run alias prints deprecation warning and still produces output: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run ./tests/fixtures/basic-mapping.yaml' + exit-code: 0 + stdout: + json: + op: "write" + user: "user:anne" + relation: "member" + object: "org:acme" + stderr: + contains: + - "deprecated"