# `Lightning.Adaptors.Catalogue`
[🔗](https://github.com/OpenFn/lightning/blob/main/lib/lightning/adaptors/catalogue.ex#L1)

Reads and writes for the `adaptors` and `adaptor_versions` tables.

Every read takes the `:source` (`:npm | :local`) explicitly.
`upsert_adaptor/1` is idempotent: `checked_at` advances on every call,
`updated_at` only when a field actually changed, and version rows are
replaced in the same transaction.

# `catalogue_entry`

```elixir
@type catalogue_entry() :: %{
  name: String.t(),
  latest_version: String.t(),
  repository: String.t() | nil,
  versions: [String.t()],
  icon_square_ext: String.t() | nil,
  icon_rectangle_ext: String.t() | nil,
  icon_square_sha256: binary() | nil,
  icon_rectangle_sha256: binary() | nil
}
```

# `package_meta`

```elixir
@type package_meta() :: %{
  name: String.t(),
  latest_version: String.t(),
  description: String.t() | nil,
  deprecated: boolean(),
  icon_square_ext: String.t() | nil,
  icon_rectangle_ext: String.t() | nil,
  icon_square_sha256: binary() | nil,
  icon_rectangle_sha256: binary() | nil,
  has_schema: boolean()
}
```

# `source`

```elixir
@type source() :: :npm | :local
```

# `catalogue`

```elixir
@spec catalogue(source()) :: [catalogue_entry()]
```

Full catalogue projection for a source: every adaptor's `name`,
`latest_version`, `repository`, icon fields, and full version list.

Excludes the packages listed in `@excluded_names` and any deprecated
adaptor. A listed adaptor's deprecated versions are left out too.

# `catalogue_stamp`

```elixir
@spec catalogue_stamp(source()) :: {DateTime.t() | nil, non_neg_integer()}
```

ETag basis for the catalogue: `{timestamp, version_row_count}` for
`source`, where `timestamp` is the later of
`MAX(adaptors.updated_at)` and `MAX(adaptor_versions.inserted_at)`,
or `nil` when the source has no rows.

`version_row_count` rides alongside the timestamp because deleting a
version row moves neither max. It does lower the count.

# `delete_all_for_source`

```elixir
@spec delete_all_for_source(source()) :: :ok
```

Delete every row for `source`.

# `get_adaptor`

```elixir
@spec get_adaptor(String.t(), source()) ::
  Lightning.Adaptors.Catalogue.Adaptor.t() | nil
```

Fetch a single adaptor by `name` within a `source`. Returns `nil`
when no row matches.

# `get_package_meta`

```elixir
@spec get_package_meta(String.t(), source()) :: package_meta() | nil
```

The `t:package_meta/0` projection of one `(name, source)` row, or `nil`.
Unlike `list_package_metas/1` this resolves excluded and deprecated
adaptors too, so jobs already using one keep validating.

# `list_adaptors`

```elixir
@spec list_adaptors(source()) :: [Lightning.Adaptors.Catalogue.Adaptor.t()]
```

Full structs for a source, for callers that need the whole row, such as
the Scheduler's diffing and the dump/seed tooling. Picker traffic uses
the lighter `list_package_metas/1`.

# `list_package_metas`

```elixir
@spec list_package_metas(source()) :: [package_meta()]
```

Picker-facing lean projection for a source. Avoids the heavy
`schema_data` JSONB column and skips the version join entirely.

Excludes the packages listed in `@excluded_names` and any deprecated
adaptor.

# `list_versions`

```elixir
@spec list_versions(String.t(), source()) :: [
  Lightning.Adaptors.Catalogue.AdaptorVersion.t()
]
```

All versions of an adaptor (`name`, `source`), in insertion order.

# `max_checked_at`

```elixir
@spec max_checked_at(source()) :: DateTime.t() | nil
```

Maximum `checked_at` seen for `source`, or `nil` when the table is
empty for that source. The Scheduler reads it at boot to time its first
tick, and the Store reads it to tell whether the catalogue has ever
loaded.

# `touch_checked_at`

```elixir
@spec touch_checked_at(String.t(), source()) :: :ok
```

Advance `checked_at` for a known `(name, source)` row without
loading it. No-op when no row matches.

The Scheduler uses this when a poll finds nothing changed. It is
cheaper than a full upsert and never bumps `updated_at`.

# `update_icons`

```elixir
@spec update_icons(String.t(), source(), map()) :: {integer(), nil}
```

Update only the icon columns for a single `(name, source)` row.

`attrs` may include any subset of `:icon_square_ext`,
`:icon_square_sha256`, `:icon_rectangle_ext`, `:icon_rectangle_sha256`,
`:icon_square_etag`, `:icon_rectangle_etag`.
`updated_at` is advanced so callers can observe the change.

Sidesteps `upsert_adaptor/1` deliberately: that helper rewrites the
`adaptor_versions` rows in the same transaction, which is the wrong
thing to do for an icon-only fix-up.

# `upsert_adaptor`

```elixir
@spec upsert_adaptor(map()) :: {:ok, Lightning.Adaptors.Catalogue.Adaptor.t()}
```

Upserts one adaptor record plus its version rows in one transaction.
The source is read from the record, whose keys may be atoms or strings,
as a decoded JSON snapshot gives them.

`checked_at` advances on every call. `updated_at` advances only when
some other field of the adaptor row differs from the existing row.
Version rows are deleted and reinserted. Every row goes through its
schema changeset first, so a corrupt strategy response cannot reach
the database.

Raises if the transaction fails, for example on invalid input from a
misbehaving strategy. The Scheduler relies on the success type only.

---

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