Skip to content

Commit 92aa732

Browse files
committed
replace ref.lid with results ids
1 parent 770aea8 commit 92aa732

File tree

3 files changed

+123
-33
lines changed

3 files changed

+123
-33
lines changed

src/JsonApiDotNetCore/Models/Operations/ResourceReference.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,8 @@
22

33
namespace JsonApiDotNetCore.Models.Operations
44
{
5-
public class ResourceReference
5+
public class ResourceReference : ResourceIdentifierObject
66
{
7-
[JsonProperty("type")]
8-
public object Type { get; set; }
9-
10-
[JsonProperty("id")]
11-
public object Id { get; set; }
12-
137
[JsonProperty("relationship")]
148
public string Relationship { get; set; }
159
}

src/JsonApiDotNetCore/Services/Operations/OperationsProcessor.cs

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public async Task<List<Operation>> ProcessAsync(List<Operation> inputOps)
3333
{
3434
var outputOps = new List<Operation>();
3535
var opIndex = 0;
36+
3637
using (var transaction = await _dbContext.Database.BeginTransactionAsync())
3738
{
3839
try
@@ -63,8 +64,8 @@ private async Task ProcessOperation(Operation op, List<Operation> outputOps)
6364
{
6465
var operationsPointer = new OperationsPointer();
6566

66-
ReplaceDataPointers(op.DataObject, outputOps);
67-
// ReplaceRefPointers(op.Ref, outputOps);
67+
ReplaceLocalIdsInResourceObject(op.DataObject, outputOps);
68+
ReplaceLocalIdsInRef(op.Ref, outputOps);
6869

6970
var processor = GetOperationsProcessor(op);
7071
var resultOp = await processor.ProcessAsync(op);
@@ -73,43 +74,56 @@ private async Task ProcessOperation(Operation op, List<Operation> outputOps)
7374
outputOps.Add(resultOp);
7475
}
7576

76-
private void ReplaceDataPointers(DocumentData data, List<Operation> outputOps)
77+
private void ReplaceLocalIdsInResourceObject(ResourceObject resourceObject, List<Operation> outputOps)
7778
{
78-
if (data == null) return;
79-
80-
bool HasLocalId(ResourceIdentifierObject rio) => string.IsNullOrEmpty(rio.LocalId) == false;
81-
string GetIdFromLocalId(string localId) {
82-
var referencedOp = outputOps.FirstOrDefault(o => o.DataObject.LocalId == localId);
83-
if(referencedOp == null) throw new JsonApiException(400, $"Could not locate lid '{localId}' in document.");
84-
return referencedOp.DataObject.Id;
85-
};
86-
87-
// are there any circumstances where the primary data would contain an lid?
88-
// if(HasLocalId(data))
89-
// {
90-
// data.Id = GetIdFromLocalId(data.LocalId);
91-
// }
92-
93-
if (data.Relationships != null)
79+
if (resourceObject == null) return;
80+
81+
// it is strange to me that a top level resource object might use a lid.
82+
// by not replacing it, we avoid a case where the first operation is an 'add' with an 'lid'
83+
// and we would be unable to locate the matching 'lid' in 'outputOps'
84+
//
85+
// we also create a scenario where I might try to update a resource I just created
86+
// in this case, the 'data.id' will be null, but the 'ref.id' will be replaced by the correct 'id' from 'outputOps'
87+
//
88+
// if(HasLocalId(resourceObject))
89+
// resourceObject.Id = GetIdFromLocalId(outputOps, resourceObject.LocalId);
90+
91+
if (resourceObject.Relationships != null)
9492
{
95-
foreach (var relationshipDictionary in data.Relationships)
93+
foreach (var relationshipDictionary in resourceObject.Relationships)
9694
{
9795
if (relationshipDictionary.Value.IsHasMany)
9896
{
9997
foreach (var relationship in relationshipDictionary.Value.ManyData)
10098
if(HasLocalId(relationship))
101-
relationship.Id = GetIdFromLocalId(relationship.LocalId);
99+
relationship.Id = GetIdFromLocalId(outputOps, relationship.LocalId);
102100
}
103101
else
104102
{
105103
var relationship = relationshipDictionary.Value.SingleData;
106104
if(HasLocalId(relationship))
107-
relationship.Id = GetIdFromLocalId(relationship.LocalId);
105+
relationship.Id = GetIdFromLocalId(outputOps, relationship.LocalId);
108106
}
109107
}
110108
}
111109
}
112110

111+
private void ReplaceLocalIdsInRef(ResourceReference resourceRef, List<Operation> outputOps)
112+
{
113+
if (resourceRef == null) return;
114+
if(HasLocalId(resourceRef))
115+
resourceRef.Id = GetIdFromLocalId(outputOps, resourceRef.LocalId);
116+
}
117+
118+
private bool HasLocalId(ResourceIdentifierObject rio) => string.IsNullOrEmpty(rio.LocalId) == false;
119+
120+
private string GetIdFromLocalId(List<Operation> outputOps, string localId)
121+
{
122+
var referencedOp = outputOps.FirstOrDefault(o => o.DataObject.LocalId == localId);
123+
if(referencedOp == null) throw new JsonApiException(400, $"Could not locate lid '{localId}' in document.");
124+
return referencedOp.DataObject.Id;
125+
}
126+
113127
private IOpProcessor GetOperationsProcessor(Operation op)
114128
{
115129
switch (op.Op)

test/UnitTests/Services/Operations/OperationsProcessorTests.cs

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public OperationsProcessorTests()
2727
}
2828

2929
[Fact]
30-
public async Task ProcessAsync_Performs_Pointer_ReplacementAsync()
30+
public async Task ProcessAsync_Performs_LocalId_ReplacementAsync_In_Relationships()
3131
{
3232
// arrange
3333
var request = @"[
@@ -89,9 +89,6 @@ public async Task ProcessAsync_Performs_Pointer_ReplacementAsync()
8989
_resolverMock.Setup(m => m.LocateCreateService(It.IsAny<Operation>()))
9090
.Returns(opProcessorMock.Object);
9191

92-
_resolverMock.Setup(m => m.LocateCreateService((It.IsAny<Operation>())))
93-
.Returns(opProcessorMock.Object);
94-
9592
_dbContextResolverMock.Setup(m => m.GetContext()).Returns(_dbContextMock.Object);
9693
var operationsProcessor = new OperationsProcessor(_resolverMock.Object, _dbContextResolverMock.Object);
9794

@@ -108,5 +105,90 @@ public async Task ProcessAsync_Performs_Pointer_ReplacementAsync()
108105
)
109106
);
110107
}
108+
109+
[Fact]
110+
public async Task ProcessAsync_Performs_LocalId_ReplacementAsync_In_References()
111+
{
112+
// arrange
113+
var request = @"[
114+
{
115+
""op"": ""add"",
116+
""data"": {
117+
""type"": ""authors"",
118+
""lid"": ""a"",
119+
""attributes"": {
120+
""name"": ""jaredcnance""
121+
}
122+
}
123+
}, {
124+
""op"": ""replace"",
125+
""ref"": {
126+
""type"": ""authors"",
127+
""lid"": ""a""
128+
},
129+
""data"": {
130+
""type"": ""authors"",
131+
""lid"": ""a"",
132+
""attributes"": {
133+
""name"": ""jnance""
134+
}
135+
}
136+
}
137+
]";
138+
139+
var op1Result = @"{
140+
""data"": {
141+
""type"": ""authors"",
142+
""id"": ""9"",
143+
""lid"": ""a"",
144+
""attributes"": {
145+
""name"": ""jaredcnance""
146+
}
147+
}
148+
}";
149+
150+
var operations = JsonConvert.DeserializeObject<List<Operation>>(request);
151+
var addOperationResult = JsonConvert.DeserializeObject<Operation>(op1Result);
152+
153+
var databaseMock = new Mock<DatabaseFacade>(_dbContextMock.Object);
154+
var transactionMock = new Mock<IDbContextTransaction>();
155+
156+
databaseMock.Setup(m => m.BeginTransactionAsync(It.IsAny<CancellationToken>()))
157+
.ReturnsAsync(transactionMock.Object);
158+
159+
_dbContextMock.Setup(m => m.Database).Returns(databaseMock.Object);
160+
161+
// setup add
162+
var addOpProcessorMock = new Mock<IOpProcessor>();
163+
addOpProcessorMock.Setup(m => m.ProcessAsync(It.Is<Operation>(op => op.DataObject.Type.ToString() == "authors")))
164+
.ReturnsAsync(addOperationResult);
165+
_resolverMock.Setup(m => m.LocateCreateService(It.IsAny<Operation>()))
166+
.Returns(addOpProcessorMock.Object);
167+
168+
// setup update
169+
var updateOpProcessorMock = new Mock<IOpProcessor>();
170+
updateOpProcessorMock.Setup(m => m.ProcessAsync(It.Is<Operation>(op => op.DataObject.Type.ToString() == "authors")))
171+
.ReturnsAsync((Operation)null);
172+
_resolverMock.Setup(m => m.LocateReplaceService(It.IsAny<Operation>()))
173+
.Returns(updateOpProcessorMock.Object);
174+
175+
_dbContextResolverMock.Setup(m => m.GetContext()).Returns(_dbContextMock.Object);
176+
var operationsProcessor = new OperationsProcessor(_resolverMock.Object, _dbContextResolverMock.Object);
177+
178+
// act
179+
var results = await operationsProcessor.ProcessAsync(operations);
180+
181+
// assert
182+
updateOpProcessorMock.Verify(
183+
m => m.ProcessAsync(
184+
It.Is<Operation>(o =>
185+
o.DataObject.Type.ToString() == "authors"
186+
// && o.DataObject.Id == "9" // currently, we will not replace the data.id member
187+
&& o.DataObject.Id == null
188+
&& o.Ref.Id == "9"
189+
)
190+
)
191+
);
192+
}
111193
}
112194
}

0 commit comments

Comments
 (0)