Skip to content

v3 #59

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 18 commits into from
Dec 9, 2019
Merged

v3 #59

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
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
language: dart
dart:
- stable
- dev
- "2.6.0"
- "2.6.1"
dart_task:
- test: --platform vm
- test: --platform chrome
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ 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]
### Added
- Support for custom non-standard links ([#61](https://github.com/f3ath/json-api-dart/issues/61))
- Client supports `jsonapi` key in outgoing requests.
- `Document.contentType` constant.
- `IdentifierObject.fromIdentifier` factory method

### Changed
Most of this changes are **BC-BREAKING**.
- `URLBuilder` was renamed to `UrlFactory`.
- `DocumentBuilder` was split into `ServerDocumentFactory` and `ClientDocumentFactory`. Some methods were renamed.
- Static `decodeJson` methods were renamed to `fromJson`.
- `Identifier.equals` now requires the runtime type to be exactly the same.
- `Link.decodeJsonMap` was renamed to `mapFromJson`
- `TargetMatcher` changed its signature.

### Removed
- (Server) `ResourceTarget`, `CollectionTarget`, `RelationshipTarget` classes.
- `QueryParameters` interface.
- `Router` class.

## [2.1.0] - 2019-12-04
### Added
Expand Down
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,22 @@ Also in this case the [Response.contentLocation]
will point to the job queue resource. You can fetch the job queue resource periodically and check
the type of the returned resource. Once the operation is complete, the request will return the created resource.

#### Adding JSON:API Object
It is possible to add the [JSON:API Object] to all documents sent by the [JsonApiClient]. To do so, pass the
pre-configured [DocumentFactory] to the [JsonApiClient]:
```dart
import 'package:http/http.dart';
import 'package:json_api/json_api.dart';

void main() async {
final api = Api(version: "1.0");
final httpClient = Client();
final jsonApiClient = JsonApiClient(httpClient, documentFactory: DocumentFactory(api: api));
}

```


# Server
The server included in this package is still under development. It is not yet suitable for real production environment
except maybe for really simple demo or testing cases.
Expand All @@ -112,14 +128,19 @@ possible request targets:
- Related resources and collections (parameterized by the resource type, resource id and the relation name)
- Relationships (parameterized by the resource type, resource id and the relation name)

The [URLBuilder] builds those 4 kinds of URLs by the given parameters. The [TargetMatcher] does the opposite,
The [UrlFactory] makes those 4 kinds of URLs by the given parameters. The [TargetMatcher] does the opposite,
it determines the target of the given URL (if possible). Together they form the [UrlDesign].

This package provides one built-in implementation of [UrlDesign] which is called [PathBasedUrlDesign].
The [PathBasedUrlDesign] implements the [Recommended URL Design] allowing you to specify the a common prefix
for all your JSON:API endpoints.


[DocumentFactory]: https://pub.dev/documentation/json_api/latest/document_factory/DocumentFactory-class.html
[Document.errors]: https://pub.dev/documentation/json_api/latest/document/Document/errors.html
[JsonApiClient]: https://pub.dev/documentation/json_api/latest/client/JsonApiClient-class.html
[PathBasedUrlDesign]: https://pub.dev/documentation/json_api/latest/url_design/PathBasedUrlDesign-class.html
[PrimaryData.included]: https://pub.dev/documentation/json_api/latest/document/PrimaryData/included.html
[Response]: https://pub.dev/documentation/json_api/latest/client/Response-class.html
[Response.data]: https://pub.dev/documentation/json_api/latest/client/Response/data.html
[Response.document]: https://pub.dev/documentation/json_api/latest/client/Response/document.html
Expand All @@ -131,14 +152,11 @@ for all your JSON:API endpoints.
[Response.status]: https://pub.dev/documentation/json_api/latest/client/Response/status.html
[Response.asyncDocument]: https://pub.dev/documentation/json_api/latest/client/Response/asyncDocument.html
[Response.asyncData]: https://pub.dev/documentation/json_api/latest/client/Response/asyncData.html

[PrimaryData.included]: https://pub.dev/documentation/json_api/latest/document/PrimaryData/included.html
[Document.errors]: https://pub.dev/documentation/json_api/latest/document/Document/errors.html
[URLBuilder]: https://pub.dev/documentation/json_api/latest/url_design/UrlBuilder-class.html
[TargetMatcher]: https://pub.dev/documentation/json_api/latest/url_design/TargetMatcher-class.html
[UrlFactory]: https://pub.dev/documentation/json_api/latest/url_design/UrlFactory-class.html
[UrlDesign]: https://pub.dev/documentation/json_api/latest/url_design/UrlDesign-class.html
[PathBasedUrlDesign]: https://pub.dev/documentation/json_api/latest/url_design/PathBasedUrlDesign-class.html

[Asynchronous Processing]: https://jsonapi.org/recommendations/#asynchronous-processing
[Compound Documents]: https://jsonapi.org/format/#document-compound-documents
[JSON:API Object]: https://jsonapi.org/format/#document-jsonapi-object
[Recommended URL Design]: https://jsonapi.org/recommendations/#urls
8 changes: 4 additions & 4 deletions example/cars_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:io';

import 'package:json_api/server.dart';
import 'package:json_api/src/document_builder.dart';
import 'package:json_api/src/server/server_document_factory.dart';
import 'package:json_api/url_design.dart';

import 'cars_server/controller.dart';
Expand Down Expand Up @@ -59,9 +59,9 @@ Future<HttpServer> createServer(InternetAddress addr, int port) async {

final httpServer = await HttpServer.bind(addr, port);
final urlDesign = PathBasedUrlDesign(Uri.parse('http://localhost:$port'));
final documentBuilder =
DocumentBuilder(urlBuilder: urlDesign, pagination: pagination);
final jsonApiServer = Server(urlDesign, controller, documentBuilder);
final documentFactory =
ServerDocumentFactory(urlDesign, pagination: pagination);
final jsonApiServer = Server(urlDesign, controller, documentFactory);

httpServer.forEach(jsonApiServer.serve);
return httpServer;
Expand Down
107 changes: 53 additions & 54 deletions example/cars_server/controller.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import 'dart:async';

import 'package:json_api/document.dart';
import 'package:json_api/server.dart';
import 'package:json_api/src/document/identifier.dart';
import 'package:json_api/src/document/json_api_error.dart';
import 'package:json_api/src/document/resource.dart';
import 'package:json_api/src/pagination/pagination.dart';
import 'package:uuid/uuid.dart';

import 'dao.dart';
Expand All @@ -18,26 +15,27 @@ class CarsController implements Controller {
CarsController(this._dao, this._pagination);

@override
Response fetchCollection(CollectionTarget target, Query query) {
final dao = _getDaoOrThrow(target);
Response fetchCollection(String type, Query query) {
final dao = _getDaoOrThrow(type);
final collection = dao.fetchCollection(
_pagination.limit(query.page), _pagination.offset(query.page));
return CollectionResponse(collection.elements.map(dao.toResource),
included: const [], total: collection.totalCount);
}

@override
Response fetchRelated(RelationshipTarget target, Query query) {
final res = _fetchResourceOrThrow(target);
Response fetchRelated(
String type, String id, String relationship, Query query) {
final res = _fetchResourceOrThrow(type, id);

if (res.toOne.containsKey(target.relationship)) {
final id = res.toOne[target.relationship];
if (res.toOne.containsKey(relationship)) {
final id = res.toOne[relationship];
final resource = _dao[id.type].fetchByIdAsResource(id.id);
return RelatedResourceResponse(resource);
}

if (res.toMany.containsKey(target.relationship)) {
final relationships = res.toMany[target.relationship];
if (res.toMany.containsKey(relationship)) {
final relationships = res.toMany[relationship];
final resources = relationships
.skip(_pagination.offset(query.page))
.take(_pagination.limit(query.page))
Expand All @@ -50,10 +48,10 @@ class CarsController implements Controller {
}

@override
Response fetchResource(ResourceTarget target, Query query) {
final dao = _getDaoOrThrow(target);
Response fetchResource(String type, String id, Query query) {
final dao = _getDaoOrThrow(type);

final obj = dao.fetchById(target.id);
final obj = dao.fetchById(id);

if (obj == null) {
return ErrorResponse.notFound(
Expand All @@ -74,43 +72,42 @@ class CarsController implements Controller {
}

@override
Response fetchRelationship(RelationshipTarget target, Query query) {
final res = _fetchResourceOrThrow(target);
Response fetchRelationship(
String type, String id, String relationship, Query query) {
final res = _fetchResourceOrThrow(type, id);

if (res.toOne.containsKey(target.relationship)) {
final id = res.toOne[target.relationship];
return ToOneResponse(target, id);
if (res.toOne.containsKey(relationship)) {
return ToOneResponse(type, id, relationship, res.toOne[relationship]);
}

if (res.toMany.containsKey(target.relationship)) {
final ids = res.toMany[target.relationship];
return ToManyResponse(target, ids);
if (res.toMany.containsKey(relationship)) {
return ToManyResponse(type, id, relationship, res.toMany[relationship]);
}
return ErrorResponse.notFound(
[JsonApiError(detail: 'Relationship not found')]);
}

@override
Response deleteResource(ResourceTarget target) {
final dao = _getDaoOrThrow(target);
Response deleteResource(String type, String id) {
final dao = _getDaoOrThrow(type);

final res = dao.fetchByIdAsResource(target.id);
final res = dao.fetchByIdAsResource(id);
if (res == null) {
throw ErrorResponse.notFound(
[JsonApiError(detail: 'Resource not found')]);
}
final dependenciesCount = dao.deleteById(target.id);
final dependenciesCount = dao.deleteById(id);
if (dependenciesCount == 0) {
return NoContentResponse();
}
return MetaResponse({'dependenciesCount': dependenciesCount});
}

@override
Response createResource(CollectionTarget target, Resource resource) {
final dao = _getDaoOrThrow(target);
Response createResource(String type, Resource resource) {
final dao = _getDaoOrThrow(type);

_throwIfIncompatibleTypes(target, resource);
_throwIfIncompatibleTypes(type, resource);

if (resource.id != null) {
if (dao.fetchById(resource.id) != null) {
Expand All @@ -126,7 +123,7 @@ class CarsController implements Controller {
toMany: resource.toMany,
toOne: resource.toOne));

if (target.type == 'models') {
if (type == 'models') {
// Insertion is artificially delayed
final job = Job(Future.delayed(Duration(milliseconds: 100), () {
dao.insert(created);
Expand All @@ -142,62 +139,64 @@ class CarsController implements Controller {
}

@override
Response updateResource(ResourceTarget target, Resource resource) {
final dao = _getDaoOrThrow(target);
Response updateResource(String type, String id, Resource resource) {
final dao = _getDaoOrThrow(type);

_throwIfIncompatibleTypes(target, resource);
if (dao.fetchById(target.id) == null) {
_throwIfIncompatibleTypes(type, resource);
if (dao.fetchById(id) == null) {
return ErrorResponse.notFound(
[JsonApiError(detail: 'Resource not found')]);
}
final updated = dao.update(target.id, resource);
final updated = dao.update(id, resource);
if (updated == null) {
return NoContentResponse();
}
return ResourceUpdatedResponse(updated);
}

@override
Response replaceToOne(RelationshipTarget target, Identifier identifier) {
final dao = _getDaoOrThrow(target);
Response replaceToOne(
String type, String id, String relationship, Identifier identifier) {
final dao = _getDaoOrThrow(type);

dao.replaceToOne(target.id, target.relationship, identifier);
dao.replaceToOne(id, relationship, identifier);
return NoContentResponse();
}

@override
Response replaceToMany(
RelationshipTarget target, List<Identifier> identifiers) {
final dao = _getDaoOrThrow(target);
Response replaceToMany(String type, String id, String relationship,
List<Identifier> identifiers) {
final dao = _getDaoOrThrow(type);

dao.replaceToMany(target.id, target.relationship, identifiers);
dao.replaceToMany(id, relationship, identifiers);
return NoContentResponse();
}

@override
Response addToMany(RelationshipTarget target, List<Identifier> identifiers) {
final dao = _getDaoOrThrow(target);
Response addToMany(String type, String id, String relationship,
List<Identifier> identifiers) {
final dao = _getDaoOrThrow(type);

return ToManyResponse(
target, dao.addToMany(target.id, target.relationship, identifiers));
type, id, relationship, dao.addToMany(id, relationship, identifiers));
}

void _throwIfIncompatibleTypes(CollectionTarget target, Resource resource) {
if (target.type != resource.type) {
void _throwIfIncompatibleTypes(String type, Resource resource) {
if (type != resource.type) {
throw ErrorResponse.conflict([JsonApiError(detail: 'Incompatible type')]);
}
}

DAO _getDaoOrThrow(CollectionTarget target) {
if (_dao.containsKey(target.type)) return _dao[target.type];
DAO _getDaoOrThrow(String type) {
if (_dao.containsKey(type)) return _dao[type];

throw ErrorResponse.notFound(
[JsonApiError(detail: 'Unknown resource type ${target.type}')]);
[JsonApiError(detail: 'Unknown resource type ${type}')]);
}

Resource _fetchResourceOrThrow(ResourceTarget target) {
final dao = _getDaoOrThrow(target);
final resource = dao.fetchByIdAsResource(target.id);
Resource _fetchResourceOrThrow(String type, String id) {
final dao = _getDaoOrThrow(type);
final resource = dao.fetchByIdAsResource(id);
if (resource == null) {
throw ErrorResponse.notFound(
[JsonApiError(detail: 'Resource not found')]);
Expand Down
3 changes: 1 addition & 2 deletions example/cars_server/dao.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:json_api/json_api.dart';
import 'package:json_api/src/nullable.dart';

import 'collection.dart';
import 'job_queue.dart';
Expand All @@ -17,7 +16,7 @@ abstract class DAO<T> {
T fetchById(String id) => _collection[id];

Resource fetchByIdAsResource(String id) =>
nullable(toResource)(_collection[id]);
_collection.containsKey(id) ? toResource(_collection[id]) : null;

void insert(T t); // => collection[t.id] = t;

Expand Down
2 changes: 1 addition & 1 deletion lib/client.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export 'package:json_api/src/client/client.dart';
export 'package:json_api/src/client/client_document_factory.dart';
export 'package:json_api/src/client/response.dart';
export 'package:json_api/src/client/simple_document_builder.dart';
export 'package:json_api/src/client/status_code.dart';
1 change: 1 addition & 0 deletions lib/query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export 'package:json_api/src/query/fields.dart';
export 'package:json_api/src/query/include.dart';
export 'package:json_api/src/query/page.dart';
export 'package:json_api/src/query/query.dart';
export 'package:json_api/src/query/sort.dart';
16 changes: 15 additions & 1 deletion lib/server.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
export 'package:json_api/query.dart';
export 'package:json_api/src/pagination/fixed_size_page.dart';
export 'package:json_api/src/pagination/pagination.dart';
export 'package:json_api/src/server/controller.dart';
export 'package:json_api/src/server/response.dart';
export 'package:json_api/src/server/response/accepted_response.dart';
export 'package:json_api/src/server/response/collection_response.dart';
export 'package:json_api/src/server/response/error_response.dart';
export 'package:json_api/src/server/response/meta_response.dart';
export 'package:json_api/src/server/response/no_content_response.dart';
export 'package:json_api/src/server/response/related_collection_response.dart';
export 'package:json_api/src/server/response/related_resource_response.dart';
export 'package:json_api/src/server/response/resource_created_response.dart';
export 'package:json_api/src/server/response/resource_response.dart';
export 'package:json_api/src/server/response/resource_updated_response.dart';
export 'package:json_api/src/server/response/response.dart';
export 'package:json_api/src/server/response/see_other_response.dart';
export 'package:json_api/src/server/response/to_many_response.dart';
export 'package:json_api/src/server/response/to_one_response.dart';
export 'package:json_api/src/server/server.dart';
export 'package:json_api/src/target.dart';
export 'package:json_api/url_design.dart';
Loading