# `ExScim.Storage.Adapter`
[🔗](https://github.com/ExScim/ex_scim/blob/main/lib/ex_scim/storage/adapter.ex#L1)

Behaviour defining the contract for all storage backends.

Implementations must handle CRUD operations for both Users and Groups.
Each callback receives an optional `scope` for multi-tenant isolation.

See `ExScim.Storage.EtsStorage` for a reference implementation, or
`ExScimEcto.StorageAdapter` for a production-ready Ecto-based backend.

## Configuration

    config :ex_scim, storage_strategy: MyApp.Storage

# `domain_group`

```elixir
@type domain_group() :: struct()
```

A domain-level group struct or map as stored by the backend.

# `domain_user`

```elixir
@type domain_user() :: struct()
```

A domain-level user struct or map as stored by the backend.

# `filter_ast`

```elixir
@type filter_ast() :: term() | nil
```

Parsed SCIM filter AST, or `nil` for no filter.

The AST is a nested tuple structure, e.g. `{:eq, "userName", "alice"}`
or `{:and, {:eq, "active", "true"}, {:co, "userName", "john"}}`.

# `group_id`

```elixir
@type group_id() :: binary()
```

Unique identifier for a group resource.

# `pagination_opts`

```elixir
@type pagination_opts() :: keyword()
```

Pagination options, e.g. `[start_index: 1, count: 20]`.

# `scope`

```elixir
@type scope() :: ExScim.Scope.t() | nil
```

Caller scope for multi-tenant isolation, or `nil`.

# `sort_opts`

```elixir
@type sort_opts() :: keyword()
```

Sort options, e.g. `[sort_by: {"userName", :asc}]`.

# `user_id`

```elixir
@type user_id() :: binary()
```

Unique identifier for a user resource.

# `create_group`

```elixir
@callback create_group(domain_group(), scope()) ::
  {:ok, domain_group()} | {:error, term()}
```

Persists a new group.

# `create_user`

```elixir
@callback create_user(domain_user(), scope()) :: {:ok, domain_user()} | {:error, term()}
```

Persists a new user.

# `delete_group`

```elixir
@callback delete_group(group_id(), scope()) :: :ok | {:error, term()}
```

Deletes a group by ID.

# `delete_user`

```elixir
@callback delete_user(user_id(), scope()) :: :ok | {:error, term()}
```

Deletes a user by ID.

# `get_group`

```elixir
@callback get_group(group_id(), scope()) :: {:ok, domain_group()} | {:error, :not_found}
```

Retrieves a single group by ID.

# `get_user`

```elixir
@callback get_user(user_id(), scope()) :: {:ok, domain_user()} | {:error, :not_found}
```

Retrieves a single user by ID.

# `group_exists?`

```elixir
@callback group_exists?(group_id(), scope()) :: boolean()
```

Returns `true` if a group with the given ID exists.

# `list_groups`

```elixir
@callback list_groups(filter_ast(), sort_opts(), pagination_opts(), scope()) ::
  {:ok, [domain_group()], non_neg_integer()}
```

Lists groups matching a filter with sorting and pagination. Returns `{:ok, groups, total_count}`.

# `list_users`

```elixir
@callback list_users(filter_ast(), sort_opts(), pagination_opts(), scope()) ::
  {:ok, [domain_user()], non_neg_integer()}
```

Lists users matching a filter with sorting and pagination. Returns `{:ok, users, total_count}`.

# `replace_group`

```elixir
@callback replace_group(group_id(), domain_group(), scope()) ::
  {:ok, domain_group()} | {:error, term()}
```

Fully replaces an existing group (PUT).

# `replace_user`

```elixir
@callback replace_user(user_id(), domain_user(), scope()) ::
  {:ok, domain_user()} | {:error, term()}
```

Fully replaces an existing user (PUT).

# `update_group`

```elixir
@callback update_group(group_id(), domain_group(), scope()) ::
  {:ok, domain_group()} | {:error, term()}
```

Partially updates an existing group (PATCH).

# `update_user`

```elixir
@callback update_user(user_id(), domain_user(), scope()) ::
  {:ok, domain_user()} | {:error, term()}
```

Partially updates an existing user (PATCH).

# `user_exists?`

```elixir
@callback user_exists?(user_id(), scope()) :: boolean()
```

Returns `true` if a user with the given ID exists.

---

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