Skip to content

v1.0.0 #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 51 commits into from
Mar 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b93b9f7
test(fetching): Request for an empty set should return an empty set
jaredcnance Feb 28, 2017
72a95ee
test(fetching): relationship request should ret null if unset
jaredcnance Feb 28, 2017
96b2efc
feat(example-models): make owner optional on todo item
jaredcnance Feb 28, 2017
e966f66
feat(example-models): db snapshot
jaredcnance Feb 28, 2017
2b3aecb
fix(*): allow null data objects
jaredcnance Feb 28, 2017
e0a9583
test(fetching): request for non-existant relationship ret 404
jaredcnance Feb 28, 2017
18f6bac
refactor(Identifiable): there is no need to make Id abstract
jaredcnance Feb 28, 2017
bbd2ccb
example(*): add todo-item-collection
jaredcnance Feb 28, 2017
8b366d9
feat(identifiable): add default for generic
jaredcnance Feb 28, 2017
8d28c80
test(inclusion): multiple relationships can be included
jaredcnance Feb 28, 2017
1dd7845
test(inclusion): 400 response if relationship does not exist
jaredcnance Feb 28, 2017
c729913
refactor(query-set): error details for nested rel
jaredcnance Feb 28, 2017
cce3149
feat(*): add page manager to hold pagination details
jaredcnance Feb 28, 2017
e6d8c3f
feat(root-links): add pagination links
jaredcnance Mar 1, 2017
1ffd68e
publish to MyGet on staging branch
jaredcnance Mar 1, 2017
7be572e
feat(ci): run builds on staging
jaredcnance Mar 1, 2017
e490683
test(*): fix test that are dependent on some items being defined
jaredcnance Mar 1, 2017
dbc1ba8
Merge pull request #42 from Research-Institute/missing-spec-tests
jaredcnance Mar 1, 2017
36e3836
docs(readme): show how to inherit non-generic Identifiable
jaredcnance Mar 1, 2017
2cc6e3a
feat(identifiable): make id member virtual
jaredcnance Mar 1, 2017
b4b1589
test(post): return 403 for client generated ids
jaredcnance Mar 1, 2017
1683c8f
test(post): response header should contain location of new resource
jaredcnance Mar 1, 2017
5edc932
test(post): server must respond 409 when processing a POST request ...
jaredcnance Mar 1, 2017
ca569fb
refactor(jsonapi-exception-filter): use exception factory
jaredcnance Mar 1, 2017
11424df
test(patch): server responds 404 if entity does not exist
jaredcnance Mar 1, 2017
43bd9e6
test(patch): can patch relationship links
jaredcnance Mar 1, 2017
87747c0
test(patch): can update to-one relationships
jaredcnance Mar 1, 2017
6b3aca6
test(delete): server returns 404 if resource does not exist
jaredcnance Mar 1, 2017
a683713
Merge pull request #43 from Research-Institute/missing-spec-tests
jaredcnance Mar 1, 2017
6fc6a1c
chore(project.json): bump package version
jaredcnance Mar 1, 2017
8572335
docs(readme): update usage documentation
jaredcnance Mar 1, 2017
e05180e
refactor(attr-attribute): move into models namespace
jaredcnance Mar 2, 2017
07ff717
feat(relationship-attr): add hasMany and hasOne attribute
jaredcnance Mar 2, 2017
0b0e8d1
feat(relationships): implement relational attrs
jaredcnance Mar 2, 2017
1fe4870
fix(example): use relationship attrs
jaredcnance Mar 2, 2017
7de22a1
refactor(generic-processor): use new attr on relationship
jaredcnance Mar 2, 2017
cf00db4
debug(travis-ci): add logging to debug travis ci
jaredcnance Mar 2, 2017
d59794e
test(meta): ensure there is at least one item in the context
jaredcnance Mar 2, 2017
9d6c4a1
clean(test/meta): remove console log statement
jaredcnance Mar 2, 2017
b247d15
Merge pull request #44 from Research-Institute/relationship-attributes
jaredcnance Mar 2, 2017
32b6a5d
test(todo-items): write failing test for like operator
jaredcnance Mar 2, 2017
dc66195
feat(filter-query): add like operator
jaredcnance Mar 2, 2017
b00e1a6
feat(IQueryableExtensions): implement the like operation
jaredcnance Mar 2, 2017
835f12b
docs(readme): document usage of new filter
jaredcnance Mar 2, 2017
ce76217
Merge pull request #45 from Research-Institute/like-filter
jaredcnance Mar 2, 2017
c669d6e
chore(project-json): add Moq
jaredcnance Mar 2, 2017
8ac10b4
test(custom): add test for authorization implementation
jaredcnance Mar 2, 2017
9d5cbc4
refactor(test): rename customization to extensibility
jaredcnance Mar 2, 2017
ba508de
docs(readme): add comment about extensibility tests
jaredcnance Mar 2, 2017
cbb06b3
fix(repository-override-test): invalid syntax
jaredcnance Mar 2, 2017
e09ee68
chore(project-json): bump package version
jaredcnance Mar 2, 2017
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
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ dotnet: 1.0.0-preview2-1-003177
branches:
only:
- master
- staging
script:
- ./build.sh
- ./build.sh
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,24 @@ Your models should inherit `Identifiable<TId>` where `TId` is the type of the pr

```csharp
public class Person : Identifiable<Guid>
{
public override Guid Id { get; set; }
{ }
```

You can use the non-generic `Identifiable` if your primary key is an integer:

```csharp
public class Person : Identifiable
{ }
```

If you need to hang annotations or attributes on the `Id` property, you can override the virtual member:

```csharp
public class Person : Identifiable
{
[Key]
[Column("person_id")]
public override int Id { get; set; }
}
```

Expand All @@ -89,8 +105,6 @@ add the `AttrAttribute` and provide the outbound name.
```csharp
public class Person : Identifiable<int>
{
public override int Id { get; set; }

[Attr("first-name")]
public string FirstName { get; set; }
}
Expand All @@ -99,16 +113,15 @@ public class Person : Identifiable<int>
#### Relationships

In order for navigation properties to be identified in the model,
they should be labeled as virtual.
they should be labeled with the appropriate attribute (either `HasOne` or `HasMany`).

```csharp
public class Person : Identifiable<int>
{
public override int Id { get; set; }

[Attr("first-name")]
public string FirstName { get; set; }

[HasMany("todo-items")]
public virtual List<TodoItem> TodoItems { get; set; }
}
```
Expand All @@ -119,12 +132,12 @@ For example, a `TodoItem` may have an `Owner` and so the Id attribute should be
```csharp
public class TodoItem : Identifiable<int>
{
public override int Id { get; set; }

[Attr("description")]
public string Description { get; set; }

public int OwnerId { get; set; }

[HasOne("owner")]
public virtual Person Owner { get; set; }
}
```
Expand Down Expand Up @@ -224,6 +237,9 @@ public class MyAuthorizedEntityRepository : DefaultEntityRepository<MyEntity>
}
```

For more examples, take a look at the customization tests
in `./test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility`.

### Pagination

Resources can be paginated.
Expand Down Expand Up @@ -272,6 +288,7 @@ identifier):
?filter[attribute]=gt:value
?filter[attribute]=le:value
?filter[attribute]=ge:value
?filter[attribute]=like:value
```

### Sorting
Expand Down
3 changes: 2 additions & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pull_requests:
branches:
only:
- master
- staging
nuget:
disable_publish_on_pr: true
build_script:
Expand All @@ -19,7 +20,7 @@ deploy:
secure: 6CeYcZ4Ze+57gxfeuHzqP6ldbUkPtF6pfpVM1Gw/K2jExFrAz763gNAQ++tiacq3
skip_symbols: true
on:
branch: master
branch: staging
- provider: NuGet
name: production
api_key:
Expand Down
32 changes: 19 additions & 13 deletions src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public Document Build(IIdentifiable entity)
var document = new Document
{
Data = _getData(contextEntity, entity),
Meta = _getMeta(entity)
Meta = _getMeta(entity),
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
};

document.Included = _appendIncludedObject(document.Included, contextEntity, entity);
Expand All @@ -45,7 +46,8 @@ public Documents Build(IEnumerable<IIdentifiable> entities)
var documents = new Documents
{
Data = new List<DocumentData>(),
Meta = _getMeta(entities.FirstOrDefault())
Meta = _getMeta(entities.FirstOrDefault()),
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
};

foreach (var entity in entities)
Expand All @@ -68,7 +70,7 @@ private Dictionary<string, object> _getMeta(IIdentifiable entity)
meta = metaEntity.GetMeta(_jsonApiContext);

if(_jsonApiContext.Options.IncludeTotalRecordCount)
meta["total-records"] = _jsonApiContext.TotalRecords;
meta["total-records"] = _jsonApiContext.PageManager.TotalRecords;

if(meta.Count > 0) return meta;
return null;
Expand Down Expand Up @@ -122,23 +124,25 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I
{
Links = new Links
{
Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.RelationshipName),
Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.RelationshipName)
Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.InternalRelationshipName),
Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.InternalRelationshipName)
}
};

if (_relationshipIsIncluded(r.RelationshipName))
if (_relationshipIsIncluded(r.InternalRelationshipName))
{
var navigationEntity = _jsonApiContext.ContextGraph
.GetRelationship(entity, r.RelationshipName);
.GetRelationship(entity, r.InternalRelationshipName);

if (navigationEntity is IEnumerable)
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.RelationshipName);
if(navigationEntity == null)
relationshipData.SingleData = null;
else if (navigationEntity is IEnumerable)
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
else
relationshipData.SingleData = _getRelationship(navigationEntity, r.RelationshipName);
relationshipData.SingleData = _getRelationship(navigationEntity, r.InternalRelationshipName);
}

data.Relationships.Add(r.RelationshipName.Dasherize(), relationshipData);
data.Relationships.Add(r.InternalRelationshipName.Dasherize(), relationshipData);
});
}

Expand All @@ -148,9 +152,9 @@ private List<DocumentData> _getIncludedEntities(ContextEntity contextEntity, IId

contextEntity.Relationships.ForEach(r =>
{
if (!_relationshipIsIncluded(r.RelationshipName)) return;
if (!_relationshipIsIncluded(r.InternalRelationshipName)) return;

var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.RelationshipName);
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName);

if (navigationEntity is IEnumerable)
foreach (var includedEntity in (IEnumerable)navigationEntity)
Expand All @@ -164,6 +168,8 @@ private List<DocumentData> _getIncludedEntities(ContextEntity contextEntity, IId

private DocumentData _getIncludedEntity(IIdentifiable entity)
{
if(entity == null) return null;

var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType());

var data = new DocumentData
Expand Down
6 changes: 5 additions & 1 deletion src/JsonApiDotNetCore/Builders/LinkBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -45,5 +44,10 @@ public string GetRelatedRelationLink(string parent, string parentId, string chil
{
return $"{_context.BasePath}/{parent.Dasherize()}/{parentId}/{child.Dasherize()}";
}

public string GetPageLink(int pageOffset, int pageSize)
{
return $"{_context.BasePath}/{_context.RequestEntity.EntityName.Dasherize()}?page[size]={pageSize}&page[number]={pageOffset}";
}
}
}
53 changes: 42 additions & 11 deletions src/JsonApiDotNetCore/Controllers/JsonApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public virtual async Task<IActionResult> GetAsync()
entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships);

if (_jsonApiContext.Options.IncludeTotalRecordCount)
_jsonApiContext.TotalRecords = await entities.CountAsync();
_jsonApiContext.PageManager.TotalRecords = await entities.CountAsync();

// pagination should be done last since it will execute the query
var pagedEntities = await ApplyPageQueryAsync(entities);
Expand Down Expand Up @@ -126,9 +126,6 @@ public virtual async Task<IActionResult> GetRelationshipAsync(TId id, string rel
var relationship = _jsonApiContext.ContextGraph
.GetRelationship<T>(entity, relationshipName);

if (relationship == null)
return NotFound();

return Ok(relationship);
}

Expand All @@ -141,9 +138,13 @@ public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
return UnprocessableEntity();
}

var stringId = entity.Id.ToString();
if(stringId.Length > 0 && stringId != "0")
return Forbidden();

await _entities.CreateAsync(entity);

return Created(HttpContext.Request.Path, entity);
return Created($"{HttpContext.Request.Path}/{entity.Id}", entity);
}

[HttpPatch("{id}")]
Expand All @@ -157,9 +158,41 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)

var updatedEntity = await _entities.UpdateAsync(id, entity);

if(updatedEntity == null) return NotFound();

return Ok(updatedEntity);
}

[HttpPatch("{id}/relationships/{relationshipName}")]
public virtual async Task<IActionResult> PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] List<DocumentData> relationships)
{
relationshipName = _jsonApiContext.ContextGraph
.GetRelationshipName<T>(relationshipName.ToProperCase());

if (relationshipName == null)
{
_logger?.LogInformation($"Relationship name not specified returning 422");
return UnprocessableEntity();
}

var entity = await _entities.GetAndIncludeAsync(id, relationshipName);

if (entity == null)
return NotFound();

var relationship = _jsonApiContext.ContextGraph
.GetContextEntity(typeof(T))
.Relationships
.FirstOrDefault(r => r.InternalRelationshipName == relationshipName);

var relationshipIds = relationships.Select(r=>r.Id);

await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds);

return Ok();

}

[HttpDelete("{id}")]
public virtual async Task<IActionResult> DeleteAsync(TId id)
{
Expand Down Expand Up @@ -190,17 +223,15 @@ private IQueryable<T> ApplySortAndFilterQuery(IQueryable<T> entities)

private async Task<IEnumerable<T>> ApplyPageQueryAsync(IQueryable<T> entities)
{
if(_jsonApiContext.Options.DefaultPageSize == 0 && (_jsonApiContext.QuerySet == null || _jsonApiContext.QuerySet.PageQuery.PageSize == 0))
var pageManager = _jsonApiContext.PageManager;
if(!pageManager.IsPaginated)
return entities;

var query = _jsonApiContext.QuerySet?.PageQuery ?? new PageQuery();

var pageNumber = query.PageOffset > 0 ? query.PageOffset : 1;
var pageSize = query.PageSize > 0 ? query.PageSize : _jsonApiContext.Options.DefaultPageSize;

_logger?.LogInformation($"Applying paging query. Fetching page {pageNumber} with {pageSize} entities");
_logger?.LogInformation($"Applying paging query. Fetching page {pageManager.CurrentPage} with {pageManager.PageSize} entities");

return await _entities.PageAsync(entities, pageSize, pageNumber);
return await _entities.PageAsync(entities, pageManager.PageSize, pageManager.CurrentPage);
}

private IQueryable<T> IncludeRelationships(IQueryable<T> entities, List<string> relationships)
Expand Down
5 changes: 5 additions & 0 deletions src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,10 @@ protected IActionResult UnprocessableEntity()
{
return new StatusCodeResult(422);
}

protected IActionResult Forbidden()
{
return new StatusCodeResult(403);
}
}
}
10 changes: 8 additions & 2 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,13 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)

await _context.SaveChangesAsync();

return oldEntity;
return oldEntity;
}

public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
{
var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.Type, _context);
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
}

public virtual async Task<bool> DeleteAsync(TId id)
Expand All @@ -125,7 +131,7 @@ public virtual async Task<bool> DeleteAsync(TId id)
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
{
var entity = _jsonApiContext.RequestEntity;
if(entity.Relationships.Any(r => r.RelationshipName == relationshipName))
if(entity.Relationships.Any(r => r.InternalRelationshipName == relationshipName))
return entities.Include(relationshipName);

throw new JsonApiException("400", "Invalid relationship",
Expand Down
3 changes: 3 additions & 0 deletions src/JsonApiDotNetCore/Data/IEntityRepository.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Internal.Query;
using JsonApiDotNetCore.Models;

Expand Down Expand Up @@ -33,6 +34,8 @@ public interface IEntityRepository<TEntity, in TId>

Task<TEntity> UpdateAsync(TId id, TEntity entity);

Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);

Task<bool> DeleteAsync(TId id);
}
}
8 changes: 7 additions & 1 deletion src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
// {1}
var right = Expression.Constant(convertedValue, property.PropertyType);

var body = Expression.Equal(left, right);
Expression body;
switch (filterQuery.FilterOperation)
{
case FilterOperations.eq:
Expand All @@ -109,6 +109,12 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
// {model.Id <= 1}
body = Expression.GreaterThanOrEqual(left, right);
break;
case FilterOperations.like:
// {model.Id <= 1}
body = Expression.Call(left, "Contains", null, right);
break;
default:
throw new JsonApiException("500", $"Unknown filter operation {filterQuery.FilterOperation}");
}

var lambda = Expression.Lambda<Func<TSource, bool>>(body, parameter);
Expand Down
4 changes: 3 additions & 1 deletion src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
var body = GetRequestBody(context.HttpContext.Request.Body);
var jsonApiContext = GetService<IJsonApiContext>(context);
var model = JsonApiDeSerializer.Deserialize(body, jsonApiContext);
var model = jsonApiContext.IsRelationshipPath ?
JsonApiDeSerializer.DeserializeRelationship(body, jsonApiContext) :
JsonApiDeSerializer.Deserialize(body, jsonApiContext);

if(model == null)
logger?.LogError("An error occurred while de-serializing the payload");
Expand Down
Loading