maksit-certs-ui/assets/docs/PATCH_DELTA_REFERENCE.md
2026-05-03 10:35:34 +02:00

251 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PATCH Delta Handling Backend & Frontend Reference
This document is the **single reference** for how PATCH payloads (deltas) are structured and interpreted so backend (BE) and frontend (FE) stay consistent. It follows the **MaksIT.Core** patch contract (same rules as shared **`deepDelta`** helpers). **MaksIT-CertsUI** uses that model with a **hostnames** collection on account PATCH.
**Audience:** Backend (C# / ASP.NET) and Frontend (TypeScript / React) developers.
---
## TL;DR (start here)
- **PATCH** sends only **what changed**, not the full resource. Each change is tagged with an **operation** (set, remove, add item, remove item).
- **Root fields** (e.g. `description`, `contact`): send new value + `operations["fieldName"] = SetField` or `RemoveField`.
- **Collections** (e.g. `hostnames`): **do not** replace the whole array when the API is “patchable collection” semantics. Send **per-item** changes: each added item has `operations.collectionItemOperation = AddToCollection`, each removed item has `RemoveFromCollection`, and changed items send identity and changed fields.
- **Frontend (Certs WebUI):** For **Edit Account**, use
`deepDelta(formState, backupState, { arrays: { hostnames: { identityKey: 'hostname', idFieldKey: 'hostname' } } })`
so hostname rows are itemized (including “add first hostname”) and stay in sync with the backend.
- **Backend:** Use `TryGetOperation(Constants.CollectionItemOperation, out var op)` on each collection item; never treat root `Operations["hostnames"] = SetField` as “replace all” if the API follows per-item patch semantics.
---
## 1. Core contract (MaksIT.Core)
The following come from **MaksIT.Core** and must be respected by all consumers.
### 1.1 PatchOperation enum
| Value | Integer | Meaning |
|-------|---------|--------|
| `SetField` | 0 | Set or replace a scalar or root-level value |
| `RemoveField` | 1 | Set a field to null |
| `AddToCollection` | 2 | Add an item to a collection (used on **collection items**, not root) |
| `RemoveFromCollection` | 3 | Remove an item from a collection (used on **collection items**, not root) |
- **Source:** `MaksIT.Core.Webapi.Models.PatchOperation`
- **FE mirror:** `PatchOperation` enum in WebUI (`src/MaksIT.WebUI/src/models/PatchOperation.ts`) must keep the same numeric values for JSON serialization.
### 1.2 PatchRequestModelBase
- **Operations:** `Dictionary<string, PatchOperation>?` (C#) / `{ [key: string]: PatchOperation }` (TS).
- **Lookup:** Case-insensitive by **property name** (e.g. `"hostnames"`, `"description"`).
- **Usage:**
- **Root level:** `Operations["propertyName"]` describes the operation for that property (e.g. `SetField` for a changed field, `RemoveField` for null).
- **Collection items:** Each element of a collection property is itself a patch model; it uses a **reserved key** (see below) to indicate add/remove/update for that item.
### 1.3 Collection item operation key
For **elements inside a collection property** (e.g. each item in `hostnames`), the operation is stored under a fixed key so the backend can distinguish “add/remove this item” from “update fields of this item”.
- **Key name:** `collectionItemOperation`
- **BE:** `Constants.CollectionItemOperation` (same string across MaksIT services when aligned with Core).
- **FE:** `COLLECTION_ITEM_OPERATION` in `src/MaksIT.WebUI/src/models/PatchOperation.ts`; same string in payloads and in `deepDelta`. Keep in sync with backend.
**Allowed values for collection items:** `AddToCollection` (2), `RemoveFromCollection` (3). For in-place updates (same item, changed fields), the item typically has an `id` and no `collectionItemOperation`, or field-level changes follow your APIs semantics.
---
## 2. Backend (BE) rules
### 2.1 Root-level properties
- Read `request.TryGetOperation(propertyName, out var op)`.
- If `op == SetField`: apply the new value from the request for that property.
- If `op == RemoveField`: set the property to null (or clear it) where applicable.
- If the property is not in `Operations` but is present with a value, treat as optional direct assignment (or ignore), depending on your API convention; for strict PATCH, prefer requiring operations for changed fields.
### 2.2 Collection properties (e.g. hostnames)
- **Rule:** Collection properties are patched **only via per-item operations** when the API follows patchable-collection semantics. The backend does **not** treat root `Operations["hostnames"] = SetField` as “replace the entire collection” unless explicitly documented for that endpoint.
- For each item in the collection payload:
1. Call `item.TryGetOperation(Constants.CollectionItemOperation, out var collectionOp)` (or the agreed constant).
2. If `collectionOp == AddToCollection`: add the item to the collection (merge by id if present, or append).
3. If `collectionOp == RemoveFromCollection`: remove the item (by `item.Id` or by matching key fields such as hostname string).
4. If no `collectionItemOperation` but the item is identifiable: treat as **in-place update**.
5. If no `collectionItemOperation` and the item cannot be identified: **do not add** ambiguous items; the FE must send `AddToCollection` for new rows when required by your rules.
### 2.3 Consistency checklist (BE)
- [ ] Use the same `CollectionItemOperation` key as the FE (see Constants / Core).
- [ ] Do not rely on root-level `SetField` for patchable collections to mean “replace all”; use per-item add/remove/update only (unless documented otherwise).
- [ ] For add: require `TryGetOperation(CollectionItemOperation) == AddToCollection` (or equivalent) for new items where applicable.
- [ ] For remove: use `RemoveFromCollection` and/or identity fields agreed with the FE.
---
## 3. Frontend (FE) rules
### 3.1 Building the delta (deepDelta)
- **Scalar / root fields:** Emit the new value and set `operations[propertyName] = SetField` or `RemoveField` as appropriate.
- **Primitive arrays:** Emit the full array and `operations[propertyName] = SetField` (full replace).
- **Object arrays that are “patchable collections”:** Must always produce **itemized deltas** when configured with an **array policy** (identity key / id field):
- Each added item must have `operations.collectionItemOperation = AddToCollection`.
- Each removed item must have `operations.collectionItemOperation = RemoveFromCollection`.
- Updated items carry changed fields and identity; see `deepDelta` implementation in `src/MaksIT.WebUI/src/functions/deep/deepDelta.ts`.
### 3.2 Patchable collections identity requirement
For the backend to interpret add/remove/update correctly, each collection **item** must be identifiable:
- **Existing items:** Use `id` from the server when present.
- **New items:** May have no server `id`; the FE must pass an **array policy** with `identityKey` / `idFieldKey` so the delta stays itemized (e.g. hostname string as stable key for hostnames).
### 3.3 Shared array policies (this repository)
**MaksIT-CertsUI** does **not** ship a shared `patchCollectionPolicies.ts`; the **Edit Account** form passes an **inline** policy for the `hostnames` collection:
| Collection | Policy (inline) | Used in |
|------------|-----------------|---------|
| `hostnames` | `{ identityKey: 'hostname', idFieldKey: 'hostname' }` | `EditAccount.tsx` |
Example:
```ts
deepDelta(fromFormState, fromBackupState, {
arrays: {
hostnames: {
identityKey: 'hostname',
idFieldKey: 'hostname',
},
},
})
```
### 3.4 Consistency checklist (FE)
- [ ] Use the same `PatchOperation` numeric values as Core (SetField 0, RemoveField 1, AddToCollection 2, RemoveFromCollection 3).
- [ ] Use `COLLECTION_ITEM_OPERATION` from `PatchOperation.ts` (same string as backend `Constants.CollectionItemOperation`).
- [ ] For account `hostnames`, pass the `hostnames` array policy in `deepDelta` so the delta is **itemized**, not a blind full-replace of the array.
- [ ] New items must have `operations.collectionItemOperation = AddToCollection` when the backend expects it.
---
## 4. Payload examples
### 4.1 Root-level SetField (scalar)
```json
{
"description": "Updated",
"operations": {
"description": 0
}
}
```
`0` = SetField.
### 4.2 Root-level RemoveField (clear optional field)
```json
{
"operations": { "someOptionalField": 1 }
}
```
`1` = RemoveField.
### 4.3 Root-level SetField (primitive array full replace)
```json
{
"tags": ["a", "b", "c"],
"operations": {
"tags": 0
}
}
```
### 4.4 Collection property itemized (add items)
Example shape for new hostname rows (numeric ops match `PatchOperation` enum):
```json
{
"hostnames": [
{
"hostname": "api.example.com",
"isDisabled": false,
"operations": { "collectionItemOperation": 2 }
}
]
}
```
`2` = AddToCollection.
### 4.5 Collection property remove item
```json
{
"hostnames": [
{
"hostname": "old.example.com",
"operations": { "collectionItemOperation": 3 }
}
]
}
```
`3` = RemoveFromCollection (identity may be `hostname` or server `id` depending on API).
### 4.6 Collection property in-place update
Item exists; fields change; no `collectionItemOperation` on the item (or only nested field operations per your model).
---
## 5. Quick reference
| Aspect | Backend | Frontend |
|--------|---------|----------|
| Operation enum | `PatchOperation` (Core) | `PatchOperation` (same values 03) |
| Root operations | `TryGetOperation(propertyName, out op)` | `operations[propertyName] = op` |
| Collection item key | `Constants.CollectionItemOperation` (`"collectionItemOperation"`) | `COLLECTION_ITEM_OPERATION` in payload and deepDelta |
| New collection item | Require `AddToCollection` on item when applicable | Send `operations.collectionItemOperation: 2` for new items |
| Certs account hostnames | Per-item patch semantics | `deepDelta` + `hostnames` array policy in `EditAccount.tsx` |
---
## 6. Related docs
- **MaksIT.Core:** `PatchOperation`, `PatchRequestModelBase` (README / XML).
---
## 7. Current implementation vs reference (MaksIT-CertsUI)
### 7.1 Frontend (FE)
**Checked:** `deepDelta` usage in `src/MaksIT.WebUI/src/forms/EditAccount.tsx` for account PATCH; inline `hostnames` array policy; `COLLECTION_ITEM_OPERATION` in `PatchOperation.ts` and usage in `deepDelta.ts`.
- **Safe:** Hostname rows use `identityKey` / `idFieldKey` so itemized deltas include `AddToCollection` / `RemoveFromCollection` where appropriate.
- **Consistent:** Same `PatchOperation` values and collection-item key string as Core.
**FE summary:** Follows the shared reference; account PATCH covers **account fields and hostnames**.
### 7.2 Backend (BE)
**Maintainers:** Confirm the account PATCH handler in MaksIT.CertsUI WebAPI applies the same per-item rules (`TryGetOperation(CollectionItemOperation, ...)`) for `hostnames` as described in sections 2.2 and 3.
### 7.3 Gaps and maintenance
| Topic | Status | Note |
|-------|--------|------|
| Shared policies file | N/A in Certs | Inline policy in `EditAccount.tsx` for `hostnames`. |
| New forms with patchable collections | **Ongoing** | When adding a form that patches a collection, pass the correct `arrays: { key: policy }` to `deepDelta`. |
---
*Last updated: 2026-04-12*