# Collections data model

Source: https://docs.qiick.io/externa/collections-data-model

Collections store structured content with a flexible **entity–attribute–value (EAV)** model: a collection defines fields, items are rows, and typed values live in a separate values table with locale and position.

> **Related**
> Field type identifiers and settings JSON are documented in [Field types](https://docs.qiick.io/externa/field-types). HTTP endpoints are covered in [Collections API](https://docs.qiick.io/externa/collections-api).

## Schema overview

```text
collections
    └── collections_fields          (schema: name, type, settings, …)
    └── collections_items           (rows; soft-deletable)
            └── collections_items_values   (EAV slots: locale, position, JSON value)
```

Migration: `database/migrations/2026_03_29_123848_create_collections_tables.php`.

### `collections`

| Column                    | Notes                                                                   |
| ------------------------- | ----------------------------------------------------------------------- |
| `id`                      | Primary key                                                             |
| `name`                    | Display name                                                            |
| `slug`                    | Unique                                                                  |
| `is_singleton`            | Boolean, default `false`                                                |
| `form_layout`             | Nullable JSON: tabs + sections grouping `field_ids` (presentation only) |
| `sort_order`              | Ordering among collections                                              |
| timestamps / soft deletes | Yes                                                                     |

Model: `App\Models\Collection`.

### `collections_fields`

| Column          | Notes                                                                                                        |
| --------------- | ------------------------------------------------------------------------------------------------------------ |
| `collection_id` | FK → collections (cascade)                                                                                   |
| `name`          | Unique per collection `(collection_id, name)`                                                                |
| `type`          | `FieldTypeEnum` string value                                                                                 |
| `translatable`  | Boolean, default `false`. Not allowed for `hash` or relation types — see [Field types](https://docs.qiick.io/externa/field-types) |
| `settings`      | JSON / jsonb bag (required, layout, type options, …)                                                         |
| `sort_order`    | Field order in forms                                                                                         |

**No soft deletes** on fields. Model: `App\Models\CollectionField`.

### `collections_items`

| Column                    | Notes                      |
| ------------------------- | -------------------------- |
| `collection_id`           | FK → collections (cascade) |
| timestamps / soft deletes | Yes                        |

Model: `App\Models\CollectionItem`. Route binding can resolve trashed items for restore / force-delete flows.

### `collections_items_values` (EAV)

| Column     | Notes                                                                   |
| ---------- | ----------------------------------------------------------------------- |
| `item_id`  | FK → items (cascade)                                                    |
| `field_id` | FK → fields (cascade)                                                   |
| `locale`   | Nullable string (max 16). Set for translatable fields; `null` otherwise |
| `position` | Unsigned smallint, default `0`. Array-valued fields use `0..n-1`        |
| `value`    | JSON / jsonb (cast to `array`/`mixed` on the model)                     |

Unique slot: `(item_id, field_id, locale, position)` as `collections_items_values_unique_slot`.

**EAV write behavior** (`CollectionItemValuesWriter`): delete all value rows for the item, then insert fresh rows; null values are skipped.

## Singleton vs multi-item

| Mode      | `is_singleton`    | Behavior                                            |
| --------- | ----------------- | --------------------------------------------------- |
| Multi     | `false` (default) | Collection has many items; UI lists items           |
| Singleton | `true`            | Exactly one content row; create seeds an empty item |

Details:

- Creating a singleton collection seeds one empty item (`ContentCollectionController::store`).
- Show route: singleton opens the collection content editor; multi redirects to the items index.
- Upsert endpoint: `PUT collections/{collection}/singleton-content`.
- `is_singleton` is **immutable** after create (`UpdateContentCollectionRequest`).
- Creating a second item on a singleton is rejected (HTTP tests + AI `ManageCollectionItems`).

## Soft deletes

| Entity              | Soft delete? | Cascade notes                                                                                                                |
| ------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| Collection          | Yes          | Soft-delete soft-deletes items; force-delete force-deletes items; restore restores only-trashed items (`Collection::booted`) |
| CollectionItem      | Yes          | Restore / force-delete routes                                                                                                |
| CollectionField     | No           | Hard-deleted with collection via FK                                                                                          |
| CollectionItemValue | No           | Cascades with item/field; writer replaces all rows on sync                                                                   |

## Locales

**Source of truth:** project settings (`content_locales`, `default_content_locale`, `fallback_content_locales`), seeded from `config/collections.php` until configured.

Shared to the SPA as `collectionLocales` / `collectionLocaleMeta` / `defaultContentLocale`.

Active locale resolution (`CollectionLocaleResolver`) considers query `locale` (must be enabled or **422**), `Accept-Language`, default content locale, then `app.locale`. Used by the normalizer, rule builder, and translated `display_name` / `note` helpers on fields.

> **Translatable fields**
> When `collections_fields.translatable` is true, item data for that field is a locale map (e.g. `{ en: "…", it: "…" }`), stored as separate value rows with `locale` set. Non-translatable fields store a single slot with `locale = null`. Disabling a content locale does not delete orphan rows.

### Locale fallback

When a reader asks for one locale, `CollectionItemDataAccessor::getTranslated` (used by Public CMS `flattenForLocale`) walks `CollectionLocaleResolver::fallbackChain`:

1. Requested / resolved locale
2. Enabled entries from `fallback_content_locales` (duplicates skipped)
3. `APP_FALLBACK_LOCALE` if that locale is enabled
4. Else `null`

`''` counts as missing. Editing Content languages in admin rewrites the stored chain; an empty list is rebuilt on save. Full detail and example: [Configuration — Locale fallback](https://docs.qiick.io/externa/configuration#locale-fallback).

## Services

All under `app/Services/Collections/`:

| Service                         | Responsibility                                                                                                                                                                                                                                                                                                                  |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CollectionItemValuesWriter`    | `sync($item, $collection, $normalizedData)` — wipe and rewrite EAV rows (per-locale and per-position for arrays)                                                                                                                                                                                                                |
| `CollectionItemValuesAssembler` | `assemble($item)` — rebuild the in-memory `data` map from value rows for forms/API                                                                                                                                                                                                                                              |
| `CollectionItemDataNormalizer`  | `normalize($collection, $data, $creating)` — coerce types (strings, numbers, bools, map GeoJSON Point/MultiPoint with legacy `{lat,lng}` compat, file/relation IDs, arrays, M2A blocks, hash auto-generation); apply transforms (`trim`, `slugify`, `lowercase`, `alphabetize`, leaf combining); fill `default_value` on create |
| `CollectionItemDataRuleBuilder` | Laravel rules for `data.*`; skips `hidden_in_form`; required from settings or validation operators; `assertKnownKeysOnly`                                                                                                                                                                                                       |
| `FieldValidationRuleEvaluator`  | Maps `settings.validation_rules` operators to Laravel/custom rules and localized messages                                                                                                                                                                                                                                       |
| `CollectionItemOptionsService`  | Paginated id/label options for relation pickers                                                                                                                                                                                                                                                                                 |
| `CollectionItemQueryService`    | Field-based filters for item listing (locale-aware for translatable fields)                                                                                                                                                                                                                                                     |

Support helpers (not under Services):

- `App\Support\Collections\CollectionItemDataAccessor` — locale fallback reads via assembler
- `App\Support\Collections\CollectionLocaleResolver` — active locale
- `App\Support\Collections\UniqueCollectionSlugGenerator` — unique slugs

## Data round-trip

```text
HTTP / Inertia payload (data: { fieldName: … })
        │
        ▼
CollectionItemDataNormalizer
        │
        ▼
CollectionItemDataRuleBuilder  (validation)
        │
        ▼
CollectionItemValuesWriter     (EAV rows)
        │
        ▼
CollectionItemValuesAssembler  (read path → data map)
```

## Related pages

- [Field types](https://docs.qiick.io/externa/field-types)
- [Collections](https://docs.qiick.io/externa/collections) / [Collection items](https://docs.qiick.io/externa/collection-items)
- [Collections API](https://docs.qiick.io/externa/collections-api)
