# `Lightning.Collaboration.WorkflowResolver`
[🔗](https://github.com/OpenFn/lightning/blob/main/lib/lightning/collaboration/workflow_resolver.ex#L1)

Single authority for resolving "given a `workflow_id` and an `action`, what
`%Workflow{}` should a collaboration session edit, and in which Ecto state?".

Both the channel join and the session save delegate here so they cannot
disagree on whether an id maps to a `:built` or `:loaded` struct — the
structural root cause behind the `workflows_pkey` duplicate INSERT on collab
reconnect (#4830).

The resolver performs **no** authorization (`Permissions.can` stays at the
channel). It does enforce project-ownership, but only when a `:project` opt is
supplied, because ownership is a property of the resolved row, not of the
user.

# `action`

```elixir
@type action() :: :new | :edit
```

# `kind`

```elixir
@type kind() :: :new | :existing | :version
```

The discriminant the resolver returns alongside the workflow, so callers
read newness from the tag rather than re-deriving it from struct shape:

  * `:new`      — `:new` action with no existing DB row (a freshly built
    `:built` struct).
  * `:existing` — a `:new` action for an id that already has a row, or an
    `:edit` action (loaded row).
  * `:version`  — a snapshot version-view (`resolve_version/3`).

# `resolve_opts`

```elixir
@type resolve_opts() :: [
  version: non_neg_integer() | nil,
  project: Lightning.Projects.Project.t() | nil
]
```

# `resolve`

```elixir
@spec resolve(
  workflow_id :: Ecto.UUID.t(),
  action :: action(),
  opts :: resolve_opts()
) ::
  {:ok, Lightning.Workflows.Workflow.t(), kind()}
  | {:error,
     :workflow_not_found
     | :wrong_project
     | :snapshot_not_found
     | :invalid_action}
```

Resolves the `%Workflow{}` a collaboration session should edit.

Returns `{:ok, workflow, kind}` where `workflow` is already in the correct
Ecto state (with `jobs`, `edges`, and `triggers` populated and the
`has_auth_method` flag set on each trigger) and `kind` is the newness
discriminant (see `t:kind/0`).

Cases:

  * `action: :new`, no existing row — a freshly built `%Workflow{}` keyed on
    `workflow_id`, with `project_id` from the supplied project, empty
    jobs/edges/triggers, and `lock_version: 0`. Ecto state `:built`, kind
    `:new`. The first-INSERT case.
  * `action: :new`, row already exists — the persisted workflow loaded with
    jobs/edges/triggers. Ecto state `:loaded`, kind `:existing`. Resolving an
    existing id to its loaded row routes the save to UPDATE rather than a
    duplicate INSERT (#4830).
  * `action: :edit`, no version, row exists — the persisted workflow loaded
    with jobs/edges/triggers and `has_auth_method` set. Ecto state `:loaded`,
    kind `:existing`.
  * `action: :edit`, no version, no row — `{:error, :workflow_not_found}`.
  * unknown action — `{:error, :invalid_action}`.

# `resolve_version`

```elixir
@spec resolve_version(
  workflow_id :: Ecto.UUID.t(),
  version :: non_neg_integer(),
  opts :: resolve_opts()
) ::
  {:ok, Lightning.Workflows.Workflow.t(), kind()}
  | {:error, :snapshot_not_found | :wrong_project | :workflow_not_found}
```

Hydrates a read-only `%Workflow{}` from a specific snapshot version.

Dispatched from `resolve/3` when a non-nil `:version` opt is present, and may
also be called directly. The `version` is a `non_neg_integer()` already parsed
by the caller — the resolver never parses version strings.

Returns a `%Workflow{}` in Ecto state `:built` carrying the snapshot's
`lock_version` and tagged with kind `:version`. A version-view is a read-only
point-in-time view that is never saved through this path; the `:version` kind
lets callers distinguish it from a genuinely-new workflow.

Sets `project_id` from the supplied project. Enforces the same
project-ownership invariant as the `:edit` latest path: when a `:project` opt
is supplied the snapshot's workflow must belong to it, otherwise
`{:error, :wrong_project}` — so a snapshot version cannot be read across
projects. With no `:project` opt (the internal point-in-time use) ownership is
not checked, matching `check_ownership/2`. Performs **no** authorization (auth
stays at the channel).

Returns `{:error, :snapshot_not_found}` when no snapshot exists for the
version, and `{:error, :workflow_not_found}` when the workflow row is gone.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
