-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRecordConflict.cs
More file actions
120 lines (103 loc) · 4.66 KB
/
Copy pathRecordConflict.cs
File metadata and controls
120 lines (103 loc) · 4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
using Honua.Sdk.Field.Records;
namespace Honua.Collect.Core.Sync;
/// <summary>
/// Which side of a field-level conflict to keep when resolving a record
/// conflict for manual review (BACKLOG S1).
/// </summary>
public enum ConflictResolution
{
/// <summary>Keep the value captured on this device.</summary>
KeepLocal,
/// <summary>Accept the value currently on the server.</summary>
KeepServer,
}
/// <summary>
/// A single field whose local value differs from the server value.
/// </summary>
/// <param name="FieldId">Field identifier.</param>
/// <param name="Label">Human-readable field label for the review UI.</param>
/// <param name="LocalValue">Value captured on this device.</param>
/// <param name="ServerValue">Value currently on the server.</param>
public sealed record FieldConflict(string FieldId, string Label, object? LocalValue, object? ServerValue);
/// <summary>
/// A version conflict between a locally-edited record and the server's current
/// version, broken down into the individual fields that differ. This is the
/// product-owned model the manual conflict-review screen binds to: the mobile
/// sync engine's <c>ManualReview</c> strategy marks an operation as conflicted,
/// and this turns the two record versions into an actionable, field-by-field
/// diff plus a merge.
/// </summary>
public sealed class RecordConflict
{
private readonly FieldRecord _local;
private readonly FieldRecord _server;
internal RecordConflict(FieldRecord local, FieldRecord server, IReadOnlyList<FieldConflict> fieldConflicts)
{
_local = local;
_server = server;
FieldConflicts = fieldConflicts;
}
/// <summary>Record identifier shared by both versions.</summary>
public string RecordId => _local.RecordId;
/// <summary>Fields whose values differ between the two versions.</summary>
public IReadOnlyList<FieldConflict> FieldConflicts { get; }
/// <summary>Whether there is anything to resolve.</summary>
public bool HasConflicts => FieldConflicts.Count > 0;
/// <summary>
/// Produces a merged record by applying a per-field resolution choice. The
/// merge starts from the union of the local and server values so server-only
/// attributes (server-computed/admin fields, or fields added after the local
/// form was cached, which never surface as <see cref="FieldConflict"/>s) are
/// preserved; each conflicted field is then overwritten with the chosen side.
/// </summary>
/// <param name="choices">Per-field resolution choices, keyed by field id.</param>
/// <param name="defaultResolution">Choice for any conflicted field absent from <paramref name="choices"/>.</param>
/// <returns>The merged record.</returns>
public FieldRecord Resolve(
IReadOnlyDictionary<string, ConflictResolution> choices,
ConflictResolution defaultResolution = ConflictResolution.KeepServer)
{
ArgumentNullException.ThrowIfNull(choices);
var merged = new FieldRecord
{
RecordId = _local.RecordId,
FormId = _local.FormId,
Location = _local.Location,
Status = _local.Status,
AssignedUserId = _local.AssignedUserId,
};
// Seed from local first, then carry over any server-only attributes that
// the local record doesn't have. Fields that genuinely differ are listed
// in FieldConflicts and resolved explicitly below, so a server attribute
// present on both sides keeps its local value here and is corrected by the
// conflict pass if it differs.
foreach (var pair in _local.Values)
{
merged.Values[pair.Key] = pair.Value;
}
foreach (var pair in _server.Values)
{
if (!merged.Values.ContainsKey(pair.Key))
{
merged.Values[pair.Key] = pair.Value;
}
}
foreach (var conflict in FieldConflicts)
{
var resolution = choices.TryGetValue(conflict.FieldId, out var choice) ? choice : defaultResolution;
merged.Values[conflict.FieldId] = resolution == ConflictResolution.KeepServer
? conflict.ServerValue
: conflict.LocalValue;
}
foreach (var media in _local.Media)
{
merged.Media.Add(media);
}
return merged;
}
/// <summary>Resolves every conflicted field to the same side.</summary>
/// <param name="resolution">The side to keep for all conflicts.</param>
/// <returns>The merged record.</returns>
public FieldRecord ResolveAll(ConflictResolution resolution)
=> Resolve(new Dictionary<string, ConflictResolution>(), resolution);
}