Skip to content

Commit

Permalink
v8 (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
f3ath authored Jul 2, 2024
1 parent 356319b commit a1b2f09
Show file tree
Hide file tree
Showing 30 changed files with 502 additions and 387 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
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).

## [8.0.0] - 2024-07-01
### Added
- CORS middware

### Changed
- Bump http\_interop to v2.0

## [7.0.1] - 2024-06-17
### Fixed
- "Accept" header with multiple values was being mishandled
Expand Down Expand Up @@ -250,6 +257,7 @@ the Document model.
### Added
- Client: fetch resources, collections, related resources and relationships

[8.0.0]: https://github.com/f3ath/json-api-dart/compare/7.0.1...8.0.0
[7.0.1]: https://github.com/f3ath/json-api-dart/compare/7.0.0...7.0.1
[7.0.0]: https://github.com/f3ath/json-api-dart/compare/6.0.1...7.0.0
[6.0.1]: https://github.com/f3ath/json-api-dart/compare/6.0.0...6.0.1
Expand Down
44 changes: 34 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

TL;DR:
```dart
import 'package:http/http.dart' as http;
import 'package:http_interop_http/http_interop_http.dart';
import 'package:json_api/client.dart';
import 'package:json_api/routing.dart';
Expand All @@ -13,29 +14,52 @@ void main() async {
/// Use the standard recommended URL structure or implement your own
final uriDesign = StandardUriDesign(Uri.parse(baseUri));
/// This is the Dart's standard HTTP client.
/// Do not forget to close it in the end.
final httpClient = http.Client();
/// This is the interface which decouples this JSON:API implementation
/// from the HTTP client.
/// Learn more: https://pub.dev/packages/http_interop
final httpHandler = httpClient.handleInterop;
/// This is the basic JSON:API client. It is flexible but not very convenient
/// to use, because you would need to remember a lot of JSON:API protocol details.
/// We will use another wrapper on top of it.
final jsonApiClient = Client(httpHandler);
/// The [RoutingClient] is most likely the right choice.
/// It has methods covering many standard use cases.
final client = RoutingClient(uriDesign, Client(OneOffHandler()));
/// It is called routing because it routes the calls to the correct
/// URLs depending on the use case. Take a look at its methods, they cover
/// all the standard scenarios specified by the JSON:API standard.
final client = RoutingClient(uriDesign, jsonApiClient);
try {
/// Fetch the collection.
/// See other methods to query and manipulate resources.
final response = await client.fetchCollection('colors');
final resources = response.collection;
resources.map((resource) => resource.attributes).forEach((attr) {
final name = attr['name'];
final red = attr['red'];
final green = attr['green'];
final blue = attr['blue'];
/// The fetched collection allows us to iterate over the resources
/// and to look into their attributes
for (final resource in response.collection) {
final {
'name': name,
'red': red,
'green': green,
'blue': blue,
} = resource.attributes;
print('${resource.type}:${resource.id}');
print('$name - $red:$green:$blue');
});
}
} on RequestFailure catch (e) {
/// Catch error response
for (var error in e.errors) {
for (final error in e.errors) {
print(error.title);
}
}
/// Free up the resources before exit.
httpClient.close();
}
```
This is a work-in-progress. You can help it by submitting a PR with a feature or documentation improvements.
Expand Down
4 changes: 2 additions & 2 deletions example/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ void main() async {
/// Do not forget to close it in the end.
final httpClient = http.Client();

/// This is the adapter which decouples this JSON:API implementation
/// This is the interface which decouples this JSON:API implementation
/// from the HTTP client.
/// Learn more: https://pub.dev/packages/http_interop
final httpHandler = ClientWrapper(httpClient);
final httpHandler = httpClient.handleInterop;

/// This is the basic JSON:API client. It is flexible but not very convenient
/// to use, because you would need to remember a lot of JSON:API protocol details.
Expand Down
10 changes: 6 additions & 4 deletions example/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ Future<void> main() async {
await initRepo(repo);
final controller = RepositoryController(repo, Uuid().v4);
interop.Handler handler =
ControllerRouter(controller, StandardUriDesign.matchTarget);
handler = TryCatchHandler(handler,
ControllerRouter(controller, StandardUriDesign.matchTarget).handle;

handler = tryCatchMiddleware(handler,
onError: ErrorConverter(onError: (e, stack) async {
stderr.writeln(e);
stderr.writeln(stack);
return Response(500,
return response(500,
document: OutboundErrorDocument(
[ErrorObject(title: 'Internal Server Error')]));
}).call);
handler = LoggingHandler(handler,

handler = loggingMiddleware(handler,
onRequest: (r) => print('${r.method} ${r.uri}'),
onResponse: (r) => print('${r.statusCode}'));
final server = JsonApiServer(handler, host: host, port: port);
Expand Down
31 changes: 0 additions & 31 deletions example/server/cors_handler.dart

This file was deleted.

2 changes: 1 addition & 1 deletion example/server/json_api_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class JsonApiServer {

Future<HttpServer> _createServer() async {
final server = await HttpServer.bind(host, port);
server.listen(listener(_handler));
server.listenInterop(_handler);
return server;
}
}
61 changes: 29 additions & 32 deletions example/server/repository_controller.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:convert';

import 'package:http_interop/extensions.dart';
import 'package:http_interop/http_interop.dart' as http;
import 'package:http_interop/http_interop.dart';
import 'package:json_api/document.dart';
import 'package:json_api/query.dart';
import 'package:json_api/routing.dart';
Expand All @@ -22,7 +22,7 @@ class RepositoryController implements Controller {
final design = StandardUriDesign.pathOnly;

@override
Future<Response> fetchCollection(http.Request request, Target target) async {
Future<Response> fetchCollection(Request request, Target target) async {
final resources = await _fetchAll(target.type).toList();
final doc = OutboundDataDocument.collection(resources)
..links['self'] = Link(design.collection(target.type));
Expand All @@ -32,128 +32,125 @@ class RepositoryController implements Controller {
doc.included.add(r);
}
}
return Response.ok(doc);
return ok(doc);
}

@override
Future<Response> fetchResource(
http.Request request, ResourceTarget target) async {
Future<Response> fetchResource(Request request, ResourceTarget target) async {
final resource = await _fetchLinkedResource(target.type, target.id);
final doc = OutboundDataDocument.resource(resource)
..links['self'] = Link(design.resource(target.type, target.id));
final forest = RelationshipNode.forest(Include.fromUri(request.uri));
await for (final r in _getAllRelated(resource, forest)) {
doc.included.add(r);
}
return Response.ok(doc);
return ok(doc);
}

@override
Future<Response> createResource(http.Request request, Target target) async {
Future<Response> createResource(Request request, Target target) async {
final document = await _decode(request);
final newResource = document.dataAsNewResource();
final res = newResource.toResource(getId);
await repo.persist(
res.type, Model(res.id)..setFrom(ModelProps.fromResource(res)));
if (newResource.id != null) {
return Response.noContent();
return noContent();
}
final ref = Reference.of(res.toIdentifier());
final self = Link(design.resource(ref.type, ref.id));
final resource = (await _fetchResource(ref.type, ref.id))
..links['self'] = self;
return Response.created(
return created(
OutboundDataDocument.resource(resource)..links['self'] = self,
self.uri.toString());
}

@override
Future<Response> addMany(
http.Request request, RelationshipTarget target) async {
Future<Response> addMany(Request request, RelationshipTarget target) async {
final many = (await _decode(request)).asRelationship<ToMany>();
final refs = await repo
.addMany(target.type, target.id, target.relationship, many)
.toList();
return Response.ok(OutboundDataDocument.many(ToMany(refs)));
return ok(OutboundDataDocument.many(ToMany(refs)));
}

@override
Future<Response> deleteResource(
http.Request request, ResourceTarget target) async {
Request request, ResourceTarget target) async {
await repo.delete(target.type, target.id);
return Response.noContent();
return noContent();
}

@override
Future<Response> updateResource(
http.Request request, ResourceTarget target) async {
Request request, ResourceTarget target) async {
await repo.update(target.type, target.id,
ModelProps.fromResource((await _decode(request)).dataAsResource()));
return Response.noContent();
return noContent();
}

@override
Future<Response> replaceRelationship(
http.Request request, RelationshipTarget target) async {
Request request, RelationshipTarget target) async {
final rel = (await _decode(request)).asRelationship();
if (rel is ToOne) {
final ref = rel.identifier;
await repo.replaceOne(target.type, target.id, target.relationship, ref);
return Response.ok(
return ok(
OutboundDataDocument.one(ref == null ? ToOne.empty() : ToOne(ref)));
}
if (rel is ToMany) {
final ids = await repo
.replaceMany(target.type, target.id, target.relationship, rel)
.toList();
return Response.ok(OutboundDataDocument.many(ToMany(ids)));
return ok(OutboundDataDocument.many(ToMany(ids)));
}
throw FormatException('Incomplete relationship');
}

@override
Future<Response> deleteMany(
http.Request request, RelationshipTarget target) async {
Request request, RelationshipTarget target) async {
final rel = (await _decode(request)).asToMany();
final ids = await repo
.deleteMany(target.type, target.id, target.relationship, rel)
.toList();
return Response.ok(OutboundDataDocument.many(ToMany(ids)));
return ok(OutboundDataDocument.many(ToMany(ids)));
}

@override
Future<Response> fetchRelationship(
http.Request request, RelationshipTarget target) async {
Request request, RelationshipTarget target) async {
final model = (await repo.fetch(target.type, target.id));

if (model.one.containsKey(target.relationship)) {
return Response.ok(OutboundDataDocument.one(
return ok(OutboundDataDocument.one(
ToOne(model.one[target.relationship]?.toIdentifier())));
}
final many =
model.many[target.relationship]?.map((it) => it.toIdentifier());
if (many != null) {
final doc = OutboundDataDocument.many(ToMany(many));
return Response.ok(doc);
return ok(doc);
}
throw RelationshipNotFound(target.type, target.id, target.relationship);
}

@override
Future<Response> fetchRelated(
http.Request request, RelatedTarget target) async {
Future<Response> fetchRelated(Request request, RelatedTarget target) async {
final model = await repo.fetch(target.type, target.id);
if (model.one.containsKey(target.relationship)) {
final related =
await nullable(_fetchRelatedResource)(model.one[target.relationship]);
final doc = OutboundDataDocument.resource(related);
return Response.ok(doc);
return ok(doc);
}
if (model.many.containsKey(target.relationship)) {
final many = model.many[target.relationship] ?? {};
final doc = OutboundDataDocument.collection(
await _fetchRelatedCollection(many).toList());
return Response.ok(doc);
return ok(doc);
}
throw RelationshipNotFound(target.type, target.id, target.relationship);
}
Expand All @@ -171,10 +168,10 @@ class RepositoryController implements Controller {

/// Returns a stream of related resources
Stream<Resource> _getRelated(Resource resource, String relationship) async* {
for (final _ in resource.relationships[relationship] ??
for (final rel in resource.relationships[relationship] ??
(throw RelationshipNotFound(
resource.type, resource.id, relationship))) {
yield await _fetchLinkedResource(_.type, _.id);
yield await _fetchLinkedResource(rel.type, rel.id);
}
}

Expand All @@ -185,7 +182,7 @@ class RepositoryController implements Controller {
}

Stream<Resource> _fetchAll(String type) =>
repo.fetchCollection(type).map((_) => _.toResource(type));
repo.fetchCollection(type).map((model) => model.toResource(type));

/// Fetches and builds a resource object
Future<Resource> _fetchResource(String type, String id) async {
Expand All @@ -203,7 +200,7 @@ class RepositoryController implements Controller {
}
}

Future<InboundDocument> _decode(http.Request r) => r.body
Future<InboundDocument> _decode(Request r) => r.body
.decode(utf8)
.then(const PayloadCodec().decode)
.then(InboundDocument.new);
Expand Down
Loading

0 comments on commit a1b2f09

Please sign in to comment.