Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions src/Umbraco.Web.UI.Client/docs/data-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,6 @@ export class UmbMyEntityDetailStore extends UmbDetailStoreBase<UmbMyEntityDetail
}
}

export default UmbMyEntityDetailStore;

export const UMB_MY_ENTITY_DETAIL_STORE_CONTEXT = new UmbContextToken<UmbMyEntityDetailStore>(
'UmbMyEntityDetailStore',
);
Expand All @@ -265,7 +263,7 @@ export class UmbMyEntityDetailRepository extends UmbDetailRepositoryBase<UmbMyEn
}
}

export default UmbMyEntityDetailRepository;
export { UmbMyEntityDetailRepository as api };
```

### 5. Workspace Context
Expand Down Expand Up @@ -376,11 +374,40 @@ Manifests are lazy-loaded — the `api: () => import(...)` pattern ensures code

---

## Client-Side Models

All layers above the data source work exclusively with **client-side domain models** — never with types from external systems (generated API types, third-party SDK types, etc.). The data source is the boundary where external types are mapped to client-side models and vice versa.

**Why:** External models are shaped by their system's concerns (API versioning, serialization format, naming conventions). Client-side models are shaped by what the UI needs. Coupling the UI to external models means changes in any external system ripple through the entire frontend.

**What to map:**

| External concept | Client-side convention |
|-----------------|----------------------|
| `id`, `key`, `path`, `isoCode`, or any identifier | `unique` (standardized across all models) |
| Entity classification | `entityType` (attached to models that represent entities) |
| Server-specific field names | Domain-appropriate property names |
| Nullable server fields with defaults | Resolved to concrete values where possible |

**Where mapping happens:** Only in data sources. The data source reads external types, constructs a client-side model, and returns it. On writes, it does the reverse — takes a client-side model and constructs the external request type.

```typescript
// Data source maps server response → client-side model
const model: UmbMyEntityDetailModel = {
entityType: UMB_MY_ENTITY_ENTITY_TYPE,
unique: data.id, // server "id" → client "unique"
};
```

**Rule:** If generated API types (`CreateXRequestModel`, `XResponseModel`, etc.) appear in a repository, context, or element import — the data source mapping layer is missing or incomplete.

---

## Key Rules

1. **Never call generated API services directly from elements or contexts** — always go through data source → repository
2. **Always use `tryExecute()` for API calls** — it handles errors and notifications
3. **Data sources handle model mapping** — they translate between backend API types and frontend domain models
3. **Data sources are the only layer that knows about external types** — they map between external system types and client-side domain models. No other layer should import or reference generated API types.
4. **All return values use `{ data, error }` tuples** — never throw from async data operations
5. **Export `default` from repositories, stores, and data sources** — this enables lazy-loading via `api: () => import(...)`
5. **Export as `api` from repositories** — classes bound to a manifest use `export { ClassName as api }`, which enables lazy-loading via `api: () => import(...)`
6. **Do not introduce new store types** — stores are being phased out. Manage state in the context layer using observable state classes directly
14 changes: 11 additions & 3 deletions src/Umbraco.Web.UI.Client/docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

How data access is organized across features. Repositories separate **where data comes from** from the UI that consumes it — the UI never knows whether data comes from a server API, a manifest, or a local cache.

For the full data flow chain (element → context → repository → data source → API client), see [Data Flow](./data-Flow.md). This document focuses on how repositories are structured, categorized, and organized within the codebase.
For the full data flow chain (element → context → repository → data source → API client), see [Data Flow](./data-flow.md). This document focuses on how repositories are structured, categorized, and organized within the codebase.

---

Expand All @@ -14,7 +14,7 @@ A repository is a **domain-specific, feature-scoped** data access layer. It prov

- **Feature-scoped** — A repository lives with the feature that uses it, both in naming and file location. A publishing repository lives inside the publishing feature folder, not in a generic repository folder.
- **Extension-registered** — Repositories are registered as `type: 'repository'` extensions with lazy-loaded `api` imports. Any extension can override a repository by registering the same alias with a higher weight.
- **Data source delegation** — The repository orchestrates but doesn't call APIs directly. It delegates to a data source that handles mapping between server types and domain models. See [Data Flow](./data-Flow.md) for the full delegation pattern.
- **Data source delegation** — The repository orchestrates but doesn't call APIs directly. It delegates to a data source that handles mapping between server types and domain models. See [Data Flow](./data-flow.md) for the full delegation pattern.
- **One concern per repository** — A detail repository handles CRUD. A publishing repository handles publish/unpublish. Don't mix concerns.

---
Expand Down Expand Up @@ -183,4 +183,12 @@ Study these when implementing repositories:
1. **Repositories live with their feature** — colocated in the feature's directory, not in a shared `repositories/` folder
2. **One concern per repository** — detail CRUD, publishing, duplication, and tree navigation are separate repositories
3. **Use base classes when they exist** — only create custom repositories for operations without a base class
4. **Always delegate to a data source** — see [Data Flow](./data-Flow.md) for the delegation pattern, `tryExecute`, and `{ data, error }` tuple conventions
4. **Always delegate to a data source** — see [Data Flow](./data-flow.md) for the delegation pattern, `tryExecute`, and `{ data, error }` tuple conventions

---

## Classify by Operations, Not by Neighbors

When creating or reviewing a repository, determine its **category** based on what operations it performs — not based on what nearby files look like. Then verify it uses the correct base class, interfaces, and data flow conventions for that category.

Use the [When to Create Which Repository Type](#when-to-create-which-repository-type) table as the decision guide. If the operations map to a standard category, the repository must use the corresponding base class and interfaces. A repository should only extend `UmbRepositoryBase` directly when no standard category fits.
Loading