Skip to content

Commit

Permalink
Merge pull request #46 from Research-Institute/staging
Browse files Browse the repository at this point in the history
v1.0.0
  • Loading branch information
jaredcnance authored Mar 2, 2017
2 parents 3c74bec + e09ee68 commit 1de9559
Show file tree
Hide file tree
Showing 64 changed files with 1,853 additions and 153 deletions.
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

0 comments on commit 1de9559

Please sign in to comment.