Skip to content

Commit

Permalink
Compound docs (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
f3ath authored Mar 18, 2019
1 parent 96bf564 commit 6eba655
Show file tree
Hide file tree
Showing 41 changed files with 1,443 additions and 958 deletions.
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

0 comments on commit 6eba655

Please sign in to comment.