Skip to content

Commit d93f021

Browse files
author
Bart Koelman
authored
Break up AttrCapabilities.AllowChange (#826)
* Break up AttrCapabilities.AllowChange Before, `AttrCapabilities.AllowChange` was used for both POST and PATCH requests (although documentation said it was only for PATCH). This commit adds a new flag and changes the meaning of the old one: - AttrCapabilities.AllowCreate affects only POST - AttrCapabilities.AllowChange affects only PATCH * Review feedback
1 parent 8bf5a22 commit d93f021

File tree

12 files changed

+124
-53
lines changed

12 files changed

+124
-53
lines changed

docs/usage/resources/attributes.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,21 @@ public class User : Identifiable<int>
5050
}
5151
```
5252

53-
### Mutability
53+
### Creatability
5454

55-
Attributes can be marked as mutable, which will allow `PATCH` requests to update them. When immutable, an HTTP 422 response is returned.
55+
Attributes can be marked as creatable, which will allow `POST` requests to assign a value to them. When sent but not allowed, an HTTP 422 response is returned.
56+
57+
```c#
58+
public class Person : Identifiable<int>
59+
{
60+
[Attr(Capabilities = AttrCapabilities.AllowCreate)]
61+
public string CreatorName { get; set; }
62+
}
63+
```
64+
65+
### Changeability
66+
67+
Attributes can be marked as changeable, which will allow `PATCH` requests to update them. When sent but not allowed, an HTTP 422 response is returned.
5668

5769
```c#
5870
public class Person : Identifiable<int>

src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public string BirthCountryName
6262
[EagerLoad]
6363
public Country BirthCountry { get; set; }
6464

65-
[Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)]
65+
[Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))]
6666
[NotMapped]
6767
public string GrantedVisaCountries => GrantedVisas == null || !GrantedVisas.Any()
6868
? null

src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public TodoItem()
2323
[Attr]
2424
public Guid GuidProperty { get; set; }
2525

26-
[Attr]
26+
[Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowCreate)]
2727
public string AlwaysChangingValue
2828
{
2929
get => Guid.NewGuid().ToString();
@@ -39,10 +39,10 @@ public string AlwaysChangingValue
3939
[Attr]
4040
public DateTime? UpdatedDate { get; set; }
4141

42-
[Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)]
42+
[Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))]
4343
public string CalculatedValue => "calculated";
4444

45-
[Attr]
45+
[Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)]
4646
public DateTimeOffset? OffsetDate { get; set; }
4747

4848
public int? OwnerId { get; set; }

src/Examples/JsonApiDotNetCoreExample/Models/User.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class User : Identifiable
1313

1414
[Attr] public string UserName { get; set; }
1515

16-
[Attr(Capabilities = AttrCapabilities.AllowChange)]
16+
[Attr(Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)]
1717
public string Password
1818
{
1919
get => _password;

src/JsonApiDotNetCore/Resources/Annotations/AttrCapabilities.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,35 @@ public enum AttrCapabilities
1111
None = 0,
1212

1313
/// <summary>
14-
/// Whether or not GET requests can return the attribute.
15-
/// Attempts to retrieve when disabled will return an HTTP 422 response.
14+
/// Whether or not GET requests can retrieve the attribute.
15+
/// Attempts to retrieve when disabled will return an HTTP 400 response.
1616
/// </summary>
1717
AllowView = 1,
1818

19+
/// <summary>
20+
/// Whether or not POST requests can assign the attribute value.
21+
/// Attempts to assign when disabled will return an HTTP 422 response.
22+
/// </summary>
23+
AllowCreate = 2,
24+
1925
/// <summary>
2026
/// Whether or not PATCH requests can update the attribute value.
2127
/// Attempts to update when disabled will return an HTTP 422 response.
2228
/// </summary>
23-
AllowChange = 2,
29+
AllowChange = 4,
2430

2531
/// <summary>
2632
/// Whether or not an attribute can be filtered on via a query string parameter.
27-
/// Attempts to sort when disabled will return an HTTP 400 response.
33+
/// Attempts to filter when disabled will return an HTTP 400 response.
2834
/// </summary>
29-
AllowFilter = 4,
35+
AllowFilter = 8,
3036

3137
/// <summary>
3238
/// Whether or not an attribute can be sorted on via a query string parameter.
3339
/// Attempts to sort when disabled will return an HTTP 400 response.
3440
/// </summary>
35-
AllowSort = 8,
41+
AllowSort = 16,
3642

37-
All = AllowView | AllowChange | AllowFilter | AllowSort
43+
All = AllowView | AllowCreate | AllowChange | AllowFilter | AllowSort
3844
}
3945
}

src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,23 @@ protected override void AfterProcessField(IIdentifiable resource, ResourceFieldA
4646
{
4747
if (field is AttrAttribute attr)
4848
{
49-
if (attr.Capabilities.HasFlag(AttrCapabilities.AllowChange))
49+
if (_httpContextAccessor.HttpContext.Request.Method == HttpMethod.Post.Method &&
50+
!attr.Capabilities.HasFlag(AttrCapabilities.AllowCreate))
5051
{
51-
_targetedFields.Attributes.Add(attr);
52+
throw new InvalidRequestBodyException(
53+
"Assigning to the requested attribute is not allowed.",
54+
$"Assigning to '{attr.PublicName}' is not allowed.", null);
5255
}
53-
else
56+
57+
if (_httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method &&
58+
!attr.Capabilities.HasFlag(AttrCapabilities.AllowChange))
5459
{
5560
throw new InvalidRequestBodyException(
5661
"Changing the value of the requested attribute is not allowed.",
5762
$"Changing the value of '{attr.PublicName}' is not allowed.", null);
5863
}
64+
65+
_targetedFields.Attributes.Add(attr);
5966
}
6067
else if (field is RelationshipAttribute relationship)
6168
_targetedFields.Relationships.Add(relationship);

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,39 @@ public async Task CreateResource_UnknownResourceType_Fails()
300300
Assert.Contains("Request body: <<", errorDocument.Errors[0].Detail);
301301
}
302302

303+
[Fact]
304+
public async Task CreateResource_Blocked_Fails()
305+
{
306+
// Arrange
307+
var content = new
308+
{
309+
data = new
310+
{
311+
type = "todoItems",
312+
attributes = new Dictionary<string, object>
313+
{
314+
{ "alwaysChangingValue", "X" }
315+
}
316+
}
317+
};
318+
319+
var requestBody = JsonConvert.SerializeObject(content);
320+
321+
// Act
322+
var (body, response) = await Post("/api/v1/todoItems", requestBody);
323+
324+
// Assert
325+
AssertEqualStatusCode(HttpStatusCode.UnprocessableEntity, response);
326+
327+
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
328+
Assert.Single(errorDocument.Errors);
329+
330+
var error = errorDocument.Errors.Single();
331+
Assert.Equal(HttpStatusCode.UnprocessableEntity, errorDocument.Errors[0].StatusCode);
332+
Assert.Equal("Failed to deserialize request body: Assigning to the requested attribute is not allowed.", error.Title);
333+
Assert.StartsWith("Assigning to 'alwaysChangingValue' is not allowed. - Request body:", error.Detail);
334+
}
335+
303336
[Fact]
304337
public async Task CreateRelationship_ToOneWithImplicitRemove_IsCreated()
305338
{

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Net;
45
using System.Net.Http;
@@ -252,6 +253,51 @@ public async Task Respond_422_If_Broken_JSON_Payload()
252253
Assert.StartsWith("Invalid character after parsing", error.Detail);
253254
}
254255

256+
[Fact]
257+
public async Task Respond_422_If_Blocked_For_Update()
258+
{
259+
// Arrange
260+
var todoItem = _todoItemFaker.Generate();
261+
_context.TodoItems.Add(todoItem);
262+
await _context.SaveChangesAsync();
263+
264+
var content = new
265+
{
266+
data = new
267+
{
268+
type = "todoItems",
269+
id = todoItem.StringId,
270+
attributes = new Dictionary<string, object>
271+
{
272+
{ "offsetDate", "2000-01-01" }
273+
}
274+
}
275+
};
276+
277+
var builder = WebHost.CreateDefaultBuilder()
278+
.UseStartup<TestStartup>();
279+
var server = new TestServer(builder);
280+
var client = server.CreateClient();
281+
282+
var requestBody = JsonConvert.SerializeObject(content);
283+
var request = PrepareRequest(HttpMethod.Patch.Method, "/api/v1/todoItems/" + todoItem.StringId, requestBody);
284+
285+
// Act
286+
var response = await client.SendAsync(request);
287+
288+
// Assert
289+
AssertEqualStatusCode(HttpStatusCode.UnprocessableEntity, response);
290+
291+
var responseBody = await response.Content.ReadAsStringAsync();
292+
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(responseBody);
293+
Assert.Single(errorDocument.Errors);
294+
295+
var error = errorDocument.Errors.Single();
296+
Assert.Equal(HttpStatusCode.UnprocessableEntity, errorDocument.Errors[0].StatusCode);
297+
Assert.Equal("Failed to deserialize request body: Changing the value of the requested attribute is not allowed.", error.Title);
298+
Assert.StartsWith("Changing the value of 'offsetDate' is not allowed. - Request body:", error.Detail);
299+
}
300+
255301
[Fact]
256302
public async Task Can_Patch_Resource()
257303
{

test/UnitTests/Serialization/Client/RequestSerializerTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ public void SerializeSingle_ResourceWithDefaultTargetFields_CanBuild()
4040
""intField"":0,
4141
""nullableIntField"":123,
4242
""guidField"":""00000000-0000-0000-0000-000000000000"",
43-
""complexField"":null,
44-
""immutable"":null
43+
""complexField"":null
4544
}
4645
}
4746
}";

test/UnitTests/Serialization/Server/RequestDeserializerTests.cs

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System.Collections.Generic;
22
using System.ComponentModel.Design;
3-
using System.Net;
4-
using JsonApiDotNetCore.Errors;
53
using JsonApiDotNetCore.Resources;
64
using JsonApiDotNetCore.Resources.Annotations;
75
using JsonApiDotNetCore.Serialization;
@@ -37,33 +35,6 @@ public void DeserializeAttributes_VariousUpdatedMembers_RegistersTargetedFields(
3735
Assert.Empty(relationshipsToUpdate);
3836
}
3937

40-
[Fact]
41-
public void DeserializeAttributes_UpdatedImmutableMember_ThrowsInvalidOperationException()
42-
{
43-
// Arrange
44-
SetupFieldsManager(out _, out _);
45-
var content = new Document
46-
{
47-
Data = new ResourceObject
48-
{
49-
Type = "testResource",
50-
Id = "1",
51-
Attributes = new Dictionary<string, object>
52-
{
53-
{ "immutable", "some string" },
54-
}
55-
}
56-
};
57-
var body = JsonConvert.SerializeObject(content);
58-
59-
// Act, assert
60-
var exception = Assert.Throws<InvalidRequestBodyException>(() => _deserializer.Deserialize(body));
61-
62-
Assert.Equal(HttpStatusCode.UnprocessableEntity, exception.Error.StatusCode);
63-
Assert.Equal("Failed to deserialize request body: Changing the value of the requested attribute is not allowed.", exception.Error.Title);
64-
Assert.Equal("Changing the value of 'immutable' is not allowed.", exception.Error.Detail);
65-
}
66-
6738
[Fact]
6839
public void DeserializeRelationships_MultipleDependentRelationships_RegistersUpdatedRelationships()
6940
{

test/UnitTests/Serialization/Server/ResponseSerializerTests.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ public void SerializeSingle_ResourceWithDefaultTargetFields_CanSerialize()
3434
""intField"":0,
3535
""nullableIntField"":123,
3636
""guidField"":""00000000-0000-0000-0000-000000000000"",
37-
""complexField"":null,
38-
""immutable"":null
37+
""complexField"":null
3938
}
4039
}
4140
}";
@@ -67,8 +66,7 @@ public void SerializeMany_ResourceWithDefaultTargetFields_CanSerialize()
6766
""intField"":0,
6867
""nullableIntField"":123,
6968
""guidField"":""00000000-0000-0000-0000-000000000000"",
70-
""complexField"":null,
71-
""immutable"":null
69+
""complexField"":null
7270
}
7371
}]
7472
}";

test/UnitTests/TestModels.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ public sealed class TestResource : Identifiable
1414
[Attr] public int? NullableIntField { get; set; }
1515
[Attr] public Guid GuidField { get; set; }
1616
[Attr] public ComplexType ComplexField { get; set; }
17-
[Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)] public string Immutable { get; set; }
1817
}
1918

2019
public class TestResourceWithList : Identifiable

0 commit comments

Comments
 (0)