Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand All @@ -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"}
Expand Down
2 changes: 1 addition & 1 deletion cmd/mapping/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,5 @@ func init() {
MappingCmd.AddCommand(validateCmd)
MappingCmd.AddCommand(testCmd)
MappingCmd.AddCommand(initCmd)
MappingCmd.AddCommand(runCmd)
MappingCmd.AddCommand(evaluateCmd)
}
73 changes: 39 additions & 34 deletions cmd/mapping/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Comment thread
ewanharris marked this conversation as resolved.
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).
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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)",
)
}
67 changes: 40 additions & 27 deletions tests/mapping-test-cases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -103,63 +103,63 @@ 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
contains:
- "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
Expand All @@ -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"
Loading