Skip to content

v8 #141

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 8 commits into from
Jul 2, 2024
Merged

v8 #141

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
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
Loading