# Data Sources

A **data source** declares an external system that the platform
ingests data from — a managed file drop, an upload endpoint,
a scheduled pull from an external SaaS, and so on. Each data
source scopes to exactly one datalake. A tenant can own many
data sources across many datalakes.

Data sources are **Platform-DB-resident**: their metadata lives
in the platform's tenant-wide DB, not in the per-datalake
schemas. As a consequence, data sources can be created against
a `new`-status datalake (no migration wait — see `datalakes.md`
§5 Lifecycle "Platform-DB / Datalake-DB split").

SDK namespace: `api.dataSources`.

## 1. Wire shape

```typescript
import type {
  DataSourceRequestWritable,
  DataSourceResponse,
} from '@alvera-ai/platform-sdk'

const body: DataSourceRequestWritable = {
  name: 'Vendor Inbound',
  uri: 'api.vendor.example.com',
  description: 'External vendor billing integration',
  status: 'active',
  is_default: false,
}

const { data: created } = await api.dataSources.create(
  tenantSlug,
  datalakeSlug,
  body,
)
// created.id        — server-derived UUID
// created.name      === body.name
// created.uri       === body.uri
// created.datalake  — references the parent datalake
```

The body is flat — no discriminator. Every data source carries
the same caller-supplied fields — four required (`name`, `uri`,
`status`, `is_default`) plus two optional (`description`,
`image_url`) — and the universal server-derived set (`id`,
`slug`, `created_at`, `updated_at`). The cookbooks omit
`image_url` (a five-field body is the common shape).

## 2. Rules the type cannot encode

### `uri` is a free-form endpoint identifier

The `uri` field is typed as `string` but the platform doesn't
enforce a strict URL grammar. Values observed include hostname
patterns (`api.stripe.com`, `appLeadCaptureForms.airtable.com`)
and host-with-port patterns (`12345.athenahealth.com`). The
field is the agent-facing identifier the platform routes against
when downstream resources (data activation clients, tools) bind
to the source; pick a stable, identifiable value the operator
can recognize.

### `is_default` is per-datalake

At most one data source per datalake carries `is_default: true`.
Promoting a new source to default demotes the prior default
(operator-mediated; the SDK doesn't expose explicit demotion).
Most SDK consumers ship sources with `is_default: false`.

### A default Manual Upload data source is auto-created per datalake

When a datalake completes migration, the platform auto-creates
a data source named `"Alvera Manual Upload - <datalake_name>"`
with `uri: "<datalake_id>.alvera.ai"` and one bound Manual
Upload tool. This row carries `is_default: true` and serves as
the default sink for ad-hoc CSV / JSON uploads.

The default row CAN be PUT-updated, but the auto-creation runs
once at datalake migration. Filter on `is_default` when
listing for management UIs that should show only consumer-
created sources.

### `status` is an enum

The `status` field carries the source's lifecycle state. The
wire enum has three values: `'draft' | 'active' | 'inactive'`.
`'active'` is the typical create-time value; `'inactive'`
short-circuits the source from new ingestion runs without
removing it; `'draft'` is the unconfigured state, useful for
multi-step UI flows.

## 3. Field ownership

**Server-derived (Response-only).** The universal set from
`type_naming.md`. Data sources add no extras.

**Caller-supplied (round-trip).** Present on both
`DataSourceRequestWritable` and `DataSourceResponse`:

```
name           required string — agent-facing label
uri            required string — endpoint identifier (free-form)
description    optional string — narrative (nullable)
image_url      optional string — icon/image URL for the source (nullable)
status         required enum   — lifecycle state
is_default     required bool   — at most one true per datalake
```

**Write-only (Request-only).** None — data sources don't hold
credentials directly. Credential handling is the consuming
resource's responsibility (e.g. tools that connect to the
source carry the authentication; the source itself is just
a name + uri).

The `slug` returned in the Response is the canonical reference
for downstream scoping. Never pre-compute it client-side — see
`type_naming.md` "Never pre-compute the slug client-side".

## 4. Error envelopes

Standard JSON:API envelopes per `errors.md`. Common rejections:

| `source.pointer`                   | Cause                                           |
|------------------------------------|-------------------------------------------------|
| `/name`                            | uniqueness within datalake (name collision)      |
| `/uri`                             | empty/structurally invalid OR uniqueness within  |
|                                    | datalake (uri collision)                          |
| `/status`                          | value not in the typed enum                      |

A structurally invalid `/name` or `/uri` (Layer 1) is a 422; a
`/name` or `/uri` **uniqueness collision** (Layer 2, mapped via
`unique_constraint`) comes back as a **409 Conflict** — same
`AlveraApiError` envelope, different status (see `errors.md` for the
422-vs-409 split). The SDK does not validate client-side.

**The `is_default` "one default per datalake" rule is the
exception — it is NOT a clean 422.** The rule is enforced only by a
Postgres partial unique index; the changeset has no matching
`unique_constraint` and the controller has no `Ecto.ConstraintError`
rescue, so creating a second `is_default: true` source raises an
**unhandled 500**, not a `/is_default` 422. Don't rely on a tidy
envelope here — read back the datalake's existing default first (§2)
and pass `is_default: false` unless you intend to be the one default.
(The distinct *delete*-the-default guard IS a real 422 — see §5.)

## 5. Lifecycle

Data sources are **Platform-DB-resident** — no async migration,
no readiness poll. `.create` returns a ready-to-use row.

### Create

```typescript
const { data: source } = await api.dataSources.create(
  tenantSlug,
  datalakeSlug,
  body,
)
// source is immediately usable as a binding target for tools,
// data activation clients, etc.
```

### Read shapes

```
.list(tenantSlug, datalakeSlug, query?)      Paged list:
                                              { data: DataSourceResponse[], meta }
                                              (full Flop query — type_naming.md)
.get(tenantSlug, datalakeSlug, id)           One row — UUID only
.metadata(tenantSlug, datalakeSlug)           Markdown string — catalog
.metadataDetails(tenantSlug, datalakeSlug,    Markdown string — one source
                 id)                          (UUID only)
```

The two `metadata` methods return `string` — agent-facing
markdown. `metadataDetails` takes the same id/slug as `.get`
(no composite key like `templates.metadataDetails`).

### Update

`update()` replaces the whole resource — resupply the full
body; there's no partial update (see `mutations.md`). Data
sources have no write-only fields, so the simple
get-then-spread pattern works (no re-supply needed):

```typescript
const { data: current } = await api.dataSources.get(
  tenantSlug, datalakeSlug, sourceId,
)
const next = { ...current, description: 'Now updated' }
const { data: updated } = await api.dataSources.update(
  tenantSlug, datalakeSlug, sourceId, next,
)
```

### Delete

```typescript
await api.dataSources.delete(tenantSlug, datalakeSlug, sourceId)
```

`delete()` removes the row and returns `204 No Content` (UUID
`id` only, like `.get` / `.update`). The platform applies
asymmetric cascade behavior by dependent resource:

- All data activation clients bound via `data_source_id`
  cascade-delete with the source (FK `on_delete: :delete_all`).
- Workflow actions referencing the data source have a
  restricting FK (`on_delete: :restrict`), so the delete is
  **rejected** while any action still points at the source —
  detach those actions first.

The auto-created default Manual Upload source (see §2 "A
default Manual Upload data source is auto-created per
datalake") is **not removable**: `delete_data_source` guards
`is_default` and returns a 422 with `is_default` → "Default data
sources cannot be deleted". The default exists for the lifetime
of its parent datalake.

If you want to retire a source without deleting it (e.g. the
default, or one a workflow action still references), mark it
`status: 'inactive'` via PUT — the row stays in the datalake but
is short-circuited from new ingestion runs.

## 6. Gotchas

1. **`.list()` is scoped to one datalake, not the tenant.** A
   data source created on datalake A never appears in
   `api.dataSources.list(tenantSlug, slugOfDatalakeB)`. The
   platform filters strictly by the datalake in the path —
   sibling-datalake sources are never surfaced in the active
   datalake's response. If you're tracking sources across
   multiple datalakes, iterate by datalake or use cross-datalake
   discovery via a higher-level surface.

2. **`is_default` collisions raise a 500, not a clean 422.** The
   "one default per datalake" rule lives only in a Postgres partial
   unique index — unmapped by the changeset and unrescued by the
   controller (see §4) — so creating a second `is_default: true`
   source currently surfaces as an unhandled 500. The SDK does NOT
   silently demote the existing default either. Check the datalake's
   current default and pass `is_default: false` unless you mean to
   own it; don't branch on a `/is_default` error envelope that
   doesn't arrive cleanly.

3. **Data sources don't carry credentials.** A source is a
   *named endpoint*, not an authenticated connection.
   Authentication for the endpoint lives on the consuming
   resource — typically a Tool's body (with its own
   `tool_body_type` discriminator). The source-tool pair is
   how connection credentials enter the system, not the
   source alone.
