Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.4.0] - 2019-03-17
### Changed
- Parsing logic moved out
- Some other BC-breaking changes in the Document
- Huge changes in the Server

### Added
- Compound documents support in Client (Server-side support is still very limited)

### Fixed
- Server was not setting links for resources and relationships

## [0.3.0] - 2019-03-16
### Changed
- Huge BC-breaking refactoring in the Document model which propagated everywhere
Expand All @@ -23,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Client: fetch resources, collections, related resources and relationships

[Unreleased]: https://github.com/f3ath/json-api-dart/compare/0.3.0...HEAD
[Unreleased]: https://github.com/f3ath/json-api-dart/compare/0.4.0...HEAD
[0.4.0]: https://github.com/f3ath/json-api-dart/compare/0.3.0...0.4.0
[0.3.0]: https://github.com/f3ath/json-api-dart/compare/0.2.0...0.3.0
[0.2.0]: https://github.com/f3ath/json-api-dart/compare/0.1.0...0.2.0
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The features here are roughly ordered by priority. Feel free to open an issue if
- [x] Updating resource's attributes
- [x] Updating resource's relationships
- [x] Updating relationships
- [ ] Compound documents
- [x] Compound documents
- [ ] Related collection pagination
- [ ] Asynchronous processing
- [ ] Optional check for `Content-Type` header in incoming responses
Expand All @@ -40,7 +40,7 @@ The features here are roughly ordered by priority. Feel free to open an issue if

#### Document
- [x] Support relationship objects lacking the `data` member
- [ ] Compound documents
- [x] Compound documents
- [ ] Support `meta` members
- [ ] Support `jsonapi` members
- [ ] Structural Validation including compound documents
Expand Down
15 changes: 3 additions & 12 deletions example/cars_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,13 @@ Future<HttpServer> createServer(InternetAddress addr, int port) async {
final controller = CarsController(
{'companies': companies, 'cities': cities, 'models': models});

final routing = StandardRouting(Uri.parse('http://localhost:$port'));
final router = StandardRouter(Uri.parse('http://localhost:$port'));

final server = JsonApiServer(routing);
final jsonApiServer = JsonApiServer(router, controller);

final httpServer = await HttpServer.bind(addr, port);

httpServer.forEach((request) async {
final route = await routing.getRoute(request.requestedUri);
if (route == null) {
request.response.statusCode = 404;
return request.response.close();
}
route.createRequest(request)
..bind(server)
..call(controller);
});
httpServer.forEach(jsonApiServer.process);

return httpServer;
}
Expand Down
202 changes: 103 additions & 99 deletions example/cars_server/controller.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'dart:async';

import 'package:json_api/document.dart';
import 'package:json_api/server.dart';
import 'package:json_api/src/server/contracts/controller.dart';
import 'package:json_api/src/server/numbered_page.dart';
import 'package:uuid/uuid.dart';

import 'dao.dart';
Expand All @@ -12,164 +13,167 @@ class CarsController implements JsonApiController {
CarsController(this.dao);

@override
Future fetchCollection(FetchCollection r) async {
if (!dao.containsKey(r.route.type)) {
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
}
final page = NumberedPage.fromQueryParameters(r.queryParameters,
total: dao[r.route.type].length);
return r.collection(
dao[r.route.type]
Future fetchCollection(FetchCollectionRequest r) async {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
final page = NumberedPage.fromQueryParameters(r.uri.queryParameters,
total: dao[r.type].length);
return r.sendCollection(
dao[r.type]
.fetchCollection(offset: page.offset)
.map(dao[r.route.type].toResource),
.map(dao[r.type].toResource),
page: page);
}

@override
Future fetchRelated(FetchRelated r) {
if (!dao.containsKey(r.route.type)) {
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
Future fetchRelated(FetchRelatedRequest r) {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
final res = dao[r.route.type].fetchByIdAsResource(r.route.id);
final res = dao[r.type].fetchByIdAsResource(r.id);
if (res == null) {
return r.notFound([JsonApiError(detail: 'Resource not found')]);
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
}

if (res.toOne.containsKey(r.route.relationship)) {
final id = res.toOne[r.route.relationship];
if (res.toOne.containsKey(r.relationship)) {
final id = res.toOne[r.relationship];
final resource = dao[id.type].fetchByIdAsResource(id.id);
return r.resource(resource);
return r.sendResource(resource);
}

if (res.toMany.containsKey(r.route.relationship)) {
final resources = res.toMany[r.route.relationship]
if (res.toMany.containsKey(r.relationship)) {
final resources = res.toMany[r.relationship]
.map((id) => dao[id.type].fetchByIdAsResource(id.id));
return r.collection(resources);
return r.sendCollection(resources);
}
return r.notFound([JsonApiError(detail: 'Relationship not found')]);
return r.errorNotFound([JsonApiError(detail: 'Relationship not found')]);
}

@override
Future fetchResource(FetchResource r) {
if (!dao.containsKey(r.route.type)) {
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
Future fetchResource(FetchResourceRequest r) {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
final res = dao[r.route.type].fetchByIdAsResource(r.route.id);
final res = dao[r.type].fetchByIdAsResource(r.id);
if (res == null) {
return r.notFound([JsonApiError(detail: 'Resource not found')]);
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
}
return r.resource(res);
final fetchById = (Identifier _) => dao[_.type].fetchByIdAsResource(_.id);

final children = res.toOne.values
.map(fetchById)
.followedBy(res.toMany.values.expand((_) => _.map(fetchById)));

return r.sendResource(res, included: children);
}

@override
Future fetchRelationship(FetchRelationship r) {
if (!dao.containsKey(r.route.type)) {
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
Future fetchRelationship(FetchRelationshipRequest r) {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
final res = dao[r.route.type].fetchByIdAsResource(r.route.id);
final res = dao[r.type].fetchByIdAsResource(r.id);
if (res == null) {
return r.notFound([JsonApiError(detail: 'Resource not found')]);
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
}

if (res.toOne.containsKey(r.route.relationship)) {
final id = res.toOne[r.route.relationship];
return r.toOne(id);
if (res.toOne.containsKey(r.relationship)) {
final id = res.toOne[r.relationship];
return r.sendToOne(id);
}

if (res.toMany.containsKey(r.route.relationship)) {
final ids = res.toMany[r.route.relationship];
return r.toMany(ids);
if (res.toMany.containsKey(r.relationship)) {
final ids = res.toMany[r.relationship];
return r.sendToMany(ids);
}
return r.notFound([JsonApiError(detail: 'Relationship not found')]);
return r.errorNotFound([JsonApiError(detail: 'Relationship not found')]);
}

@override
Future deleteResource(DeleteResource r) {
if (!dao.containsKey(r.route.type)) {
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
Future deleteResource(DeleteResourceRequest r) {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
final res = dao[r.route.type].fetchByIdAsResource(r.route.id);
final res = dao[r.type].fetchByIdAsResource(r.id);
if (res == null) {
return r.notFound([JsonApiError(detail: 'Resource not found')]);
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
}
final dependenciesCount = dao[r.route.type].deleteById(r.route.id);
final dependenciesCount = dao[r.type].deleteById(r.id);
if (dependenciesCount == 0) {
return r.noContent();
return r.sendNoContent();
}
return r.meta({'dependenciesCount': dependenciesCount});
return r.sendMeta({'dependenciesCount': dependenciesCount});
}

Future createResource(CreateResource r) async {
if (!dao.containsKey(r.route.type)) {
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
Future createResource(CreateResourceRequest r) async {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
final resource = await r.resource();
if (r.route.type != resource.type) {
return r.conflict([JsonApiError(detail: 'Incompatible type')]);
if (r.type != r.resource.type) {
return r.errorConflict([JsonApiError(detail: 'Incompatible type')]);
}

if (resource.hasId) {
if (dao[r.route.type].fetchById(resource.id) != null) {
return r.conflict([JsonApiError(detail: 'Resource already exists')]);
if (r.resource.hasId) {
if (dao[r.type].fetchById(r.resource.id) != null) {
return r
.errorConflict([JsonApiError(detail: 'Resource already exists')]);
}
final created = dao[r.route.type].create(resource);
dao[r.route.type].insert(created);
return r.noContent();
}

final created = dao[r.route.type].create(Resource(
resource.type, Uuid().v4(),
attributes: resource.attributes,
toMany: resource.toMany,
toOne: resource.toOne));
dao[r.route.type].insert(created);
return r.created(dao[r.route.type].toResource(created));
final created = dao[r.type].create(r.resource);
dao[r.type].insert(created);
return r.sendNoContent();
}

final created = dao[r.type].create(Resource(r.resource.type, Uuid().v4(),
attributes: r.resource.attributes,
toMany: r.resource.toMany,
toOne: r.resource.toOne));
dao[r.type].insert(created);
return r.sendCreated(dao[r.type].toResource(created));
}

@override
Future updateResource(UpdateResource r) async {
if (!dao.containsKey(r.route.type)) {
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
Future updateResource(UpdateResourceRequest r) async {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
final resource = await r.resource();
if (r.route.type != resource.type) {
return r.conflict([JsonApiError(detail: 'Incompatible type')]);
if (r.type != r.resource.type) {
return r.errorConflict([JsonApiError(detail: 'Incompatible type')]);
}
if (dao[r.route.type].fetchById(r.route.id) == null) {
return r.notFound([JsonApiError(detail: 'Resource not found')]);
if (dao[r.type].fetchById(r.id) == null) {
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
}
final updated = dao[r.route.type].update(r.route.id, resource);
final updated = dao[r.type].update(r.id, r.resource);
if (updated == null) {
return r.noContent();
return r.sendNoContent();
}
return r.updated(updated);
return r.sendUpdated(updated);
}

@override
Future replaceRelationship(ReplaceRelationship r) async {
if (!dao.containsKey(r.route.type)) {
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
Future replaceToOne(ReplaceToOneRequest r) async {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
final rel = await r.relationshipData();
if (rel is ToOne) {
dao[r.route.type]
.replaceToOne(r.route.id, r.route.relationship, rel.toIdentifier());
return r.noContent();
}
if (rel is ToMany) {
dao[r.route.type]
.replaceToMany(r.route.id, r.route.relationship, rel.identifiers);
return r.noContent();
dao[r.type].replaceToOne(r.id, r.relationship, r.identifier);
return r.sendNoContent();
}

@override
Future replaceToMany(ReplaceToManyRequest r) async {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
dao[r.type].replaceToMany(r.id, r.relationship, r.identifiers);
return r.sendNoContent();
}

@override
Future addToRelationship(AddToRelationship r) async {
if (!dao.containsKey(r.route.type)) {
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
Future addToMany(AddToManyRequest r) async {
if (!dao.containsKey(r.type)) {
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
}
final result = dao[r.route.type]
.addToMany(r.route.id, r.route.relationship, await r.identifiers());
return r.toMany(result);
final result = dao[r.type].addToMany(r.id, r.relationship, r.identifiers);
return r.sendToMany(result);
}
}
6 changes: 4 additions & 2 deletions lib/document.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export 'package:json_api/src/document/document.dart';
export 'package:json_api/src/document/error.dart';
export 'package:json_api/src/document/identifier.dart';
export 'package:json_api/src/document/identifier_json.dart';
export 'package:json_api/src/document/identifier_object.dart';
export 'package:json_api/src/document/link.dart';
export 'package:json_api/src/document/primary_data.dart';
export 'package:json_api/src/document/relationship.dart';
export 'package:json_api/src/document/resource.dart';
export 'package:json_api/src/document/resource_json.dart';
export 'package:json_api/src/document/resource_collection_data.dart';
export 'package:json_api/src/document/resource_data.dart';
export 'package:json_api/src/document/resource_object.dart';
Loading