Skip to content

Commit 6eba655

Browse files
authored
Compound docs (#30)
1 parent 96bf564 commit 6eba655

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1443
-958
lines changed

CHANGELOG.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
9+
## [0.4.0] - 2019-03-17
10+
### Changed
11+
- Parsing logic moved out
12+
- Some other BC-breaking changes in the Document
13+
- Huge changes in the Server
14+
15+
### Added
16+
- Compound documents support in Client (Server-side support is still very limited)
17+
18+
### Fixed
19+
- Server was not setting links for resources and relationships
20+
821
## [0.3.0] - 2019-03-16
922
### Changed
1023
- Huge BC-breaking refactoring in the Document model which propagated everywhere
@@ -23,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2336
### Added
2437
- Client: fetch resources, collections, related resources and relationships
2538

26-
[Unreleased]: https://github.com/f3ath/json-api-dart/compare/0.3.0...HEAD
39+
[Unreleased]: https://github.com/f3ath/json-api-dart/compare/0.4.0...HEAD
40+
[0.4.0]: https://github.com/f3ath/json-api-dart/compare/0.3.0...0.4.0
2741
[0.3.0]: https://github.com/f3ath/json-api-dart/compare/0.2.0...0.3.0
2842
[0.2.0]: https://github.com/f3ath/json-api-dart/compare/0.1.0...0.2.0

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The features here are roughly ordered by priority. Feel free to open an issue if
1515
- [x] Updating resource's attributes
1616
- [x] Updating resource's relationships
1717
- [x] Updating relationships
18-
- [ ] Compound documents
18+
- [x] Compound documents
1919
- [ ] Related collection pagination
2020
- [ ] Asynchronous processing
2121
- [ ] Optional check for `Content-Type` header in incoming responses
@@ -40,7 +40,7 @@ The features here are roughly ordered by priority. Feel free to open an issue if
4040

4141
#### Document
4242
- [x] Support relationship objects lacking the `data` member
43-
- [ ] Compound documents
43+
- [x] Compound documents
4444
- [ ] Support `meta` members
4545
- [ ] Support `jsonapi` members
4646
- [ ] Structural Validation including compound documents

example/cars_server.dart

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,13 @@ Future<HttpServer> createServer(InternetAddress addr, int port) async {
4949
final controller = CarsController(
5050
{'companies': companies, 'cities': cities, 'models': models});
5151

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

54-
final server = JsonApiServer(routing);
54+
final jsonApiServer = JsonApiServer(router, controller);
5555

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

58-
httpServer.forEach((request) async {
59-
final route = await routing.getRoute(request.requestedUri);
60-
if (route == null) {
61-
request.response.statusCode = 404;
62-
return request.response.close();
63-
}
64-
route.createRequest(request)
65-
..bind(server)
66-
..call(controller);
67-
});
58+
httpServer.forEach(jsonApiServer.process);
6859

6960
return httpServer;
7061
}
Lines changed: 103 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import 'dart:async';
22

33
import 'package:json_api/document.dart';
4-
import 'package:json_api/server.dart';
4+
import 'package:json_api/src/server/contracts/controller.dart';
5+
import 'package:json_api/src/server/numbered_page.dart';
56
import 'package:uuid/uuid.dart';
67

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

1415
@override
15-
Future fetchCollection(FetchCollection r) async {
16-
if (!dao.containsKey(r.route.type)) {
17-
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
18-
}
19-
final page = NumberedPage.fromQueryParameters(r.queryParameters,
20-
total: dao[r.route.type].length);
21-
return r.collection(
22-
dao[r.route.type]
16+
Future fetchCollection(FetchCollectionRequest r) async {
17+
if (!dao.containsKey(r.type)) {
18+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
19+
}
20+
final page = NumberedPage.fromQueryParameters(r.uri.queryParameters,
21+
total: dao[r.type].length);
22+
return r.sendCollection(
23+
dao[r.type]
2324
.fetchCollection(offset: page.offset)
24-
.map(dao[r.route.type].toResource),
25+
.map(dao[r.type].toResource),
2526
page: page);
2627
}
2728

2829
@override
29-
Future fetchRelated(FetchRelated r) {
30-
if (!dao.containsKey(r.route.type)) {
31-
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
30+
Future fetchRelated(FetchRelatedRequest r) {
31+
if (!dao.containsKey(r.type)) {
32+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
3233
}
33-
final res = dao[r.route.type].fetchByIdAsResource(r.route.id);
34+
final res = dao[r.type].fetchByIdAsResource(r.id);
3435
if (res == null) {
35-
return r.notFound([JsonApiError(detail: 'Resource not found')]);
36+
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
3637
}
3738

38-
if (res.toOne.containsKey(r.route.relationship)) {
39-
final id = res.toOne[r.route.relationship];
39+
if (res.toOne.containsKey(r.relationship)) {
40+
final id = res.toOne[r.relationship];
4041
final resource = dao[id.type].fetchByIdAsResource(id.id);
41-
return r.resource(resource);
42+
return r.sendResource(resource);
4243
}
4344

44-
if (res.toMany.containsKey(r.route.relationship)) {
45-
final resources = res.toMany[r.route.relationship]
45+
if (res.toMany.containsKey(r.relationship)) {
46+
final resources = res.toMany[r.relationship]
4647
.map((id) => dao[id.type].fetchByIdAsResource(id.id));
47-
return r.collection(resources);
48+
return r.sendCollection(resources);
4849
}
49-
return r.notFound([JsonApiError(detail: 'Relationship not found')]);
50+
return r.errorNotFound([JsonApiError(detail: 'Relationship not found')]);
5051
}
5152

5253
@override
53-
Future fetchResource(FetchResource r) {
54-
if (!dao.containsKey(r.route.type)) {
55-
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
54+
Future fetchResource(FetchResourceRequest r) {
55+
if (!dao.containsKey(r.type)) {
56+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
5657
}
57-
final res = dao[r.route.type].fetchByIdAsResource(r.route.id);
58+
final res = dao[r.type].fetchByIdAsResource(r.id);
5859
if (res == null) {
59-
return r.notFound([JsonApiError(detail: 'Resource not found')]);
60+
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
6061
}
61-
return r.resource(res);
62+
final fetchById = (Identifier _) => dao[_.type].fetchByIdAsResource(_.id);
63+
64+
final children = res.toOne.values
65+
.map(fetchById)
66+
.followedBy(res.toMany.values.expand((_) => _.map(fetchById)));
67+
68+
return r.sendResource(res, included: children);
6269
}
6370

6471
@override
65-
Future fetchRelationship(FetchRelationship r) {
66-
if (!dao.containsKey(r.route.type)) {
67-
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
72+
Future fetchRelationship(FetchRelationshipRequest r) {
73+
if (!dao.containsKey(r.type)) {
74+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
6875
}
69-
final res = dao[r.route.type].fetchByIdAsResource(r.route.id);
76+
final res = dao[r.type].fetchByIdAsResource(r.id);
7077
if (res == null) {
71-
return r.notFound([JsonApiError(detail: 'Resource not found')]);
78+
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
7279
}
7380

74-
if (res.toOne.containsKey(r.route.relationship)) {
75-
final id = res.toOne[r.route.relationship];
76-
return r.toOne(id);
81+
if (res.toOne.containsKey(r.relationship)) {
82+
final id = res.toOne[r.relationship];
83+
return r.sendToOne(id);
7784
}
7885

79-
if (res.toMany.containsKey(r.route.relationship)) {
80-
final ids = res.toMany[r.route.relationship];
81-
return r.toMany(ids);
86+
if (res.toMany.containsKey(r.relationship)) {
87+
final ids = res.toMany[r.relationship];
88+
return r.sendToMany(ids);
8289
}
83-
return r.notFound([JsonApiError(detail: 'Relationship not found')]);
90+
return r.errorNotFound([JsonApiError(detail: 'Relationship not found')]);
8491
}
8592

8693
@override
87-
Future deleteResource(DeleteResource r) {
88-
if (!dao.containsKey(r.route.type)) {
89-
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
94+
Future deleteResource(DeleteResourceRequest r) {
95+
if (!dao.containsKey(r.type)) {
96+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
9097
}
91-
final res = dao[r.route.type].fetchByIdAsResource(r.route.id);
98+
final res = dao[r.type].fetchByIdAsResource(r.id);
9299
if (res == null) {
93-
return r.notFound([JsonApiError(detail: 'Resource not found')]);
100+
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
94101
}
95-
final dependenciesCount = dao[r.route.type].deleteById(r.route.id);
102+
final dependenciesCount = dao[r.type].deleteById(r.id);
96103
if (dependenciesCount == 0) {
97-
return r.noContent();
104+
return r.sendNoContent();
98105
}
99-
return r.meta({'dependenciesCount': dependenciesCount});
106+
return r.sendMeta({'dependenciesCount': dependenciesCount});
100107
}
101108

102-
Future createResource(CreateResource r) async {
103-
if (!dao.containsKey(r.route.type)) {
104-
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
109+
Future createResource(CreateResourceRequest r) async {
110+
if (!dao.containsKey(r.type)) {
111+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
105112
}
106-
final resource = await r.resource();
107-
if (r.route.type != resource.type) {
108-
return r.conflict([JsonApiError(detail: 'Incompatible type')]);
113+
if (r.type != r.resource.type) {
114+
return r.errorConflict([JsonApiError(detail: 'Incompatible type')]);
109115
}
110116

111-
if (resource.hasId) {
112-
if (dao[r.route.type].fetchById(resource.id) != null) {
113-
return r.conflict([JsonApiError(detail: 'Resource already exists')]);
117+
if (r.resource.hasId) {
118+
if (dao[r.type].fetchById(r.resource.id) != null) {
119+
return r
120+
.errorConflict([JsonApiError(detail: 'Resource already exists')]);
114121
}
115-
final created = dao[r.route.type].create(resource);
116-
dao[r.route.type].insert(created);
117-
return r.noContent();
118-
}
119-
120-
final created = dao[r.route.type].create(Resource(
121-
resource.type, Uuid().v4(),
122-
attributes: resource.attributes,
123-
toMany: resource.toMany,
124-
toOne: resource.toOne));
125-
dao[r.route.type].insert(created);
126-
return r.created(dao[r.route.type].toResource(created));
122+
final created = dao[r.type].create(r.resource);
123+
dao[r.type].insert(created);
124+
return r.sendNoContent();
125+
}
126+
127+
final created = dao[r.type].create(Resource(r.resource.type, Uuid().v4(),
128+
attributes: r.resource.attributes,
129+
toMany: r.resource.toMany,
130+
toOne: r.resource.toOne));
131+
dao[r.type].insert(created);
132+
return r.sendCreated(dao[r.type].toResource(created));
127133
}
128134

129135
@override
130-
Future updateResource(UpdateResource r) async {
131-
if (!dao.containsKey(r.route.type)) {
132-
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
136+
Future updateResource(UpdateResourceRequest r) async {
137+
if (!dao.containsKey(r.type)) {
138+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
133139
}
134-
final resource = await r.resource();
135-
if (r.route.type != resource.type) {
136-
return r.conflict([JsonApiError(detail: 'Incompatible type')]);
140+
if (r.type != r.resource.type) {
141+
return r.errorConflict([JsonApiError(detail: 'Incompatible type')]);
137142
}
138-
if (dao[r.route.type].fetchById(r.route.id) == null) {
139-
return r.notFound([JsonApiError(detail: 'Resource not found')]);
143+
if (dao[r.type].fetchById(r.id) == null) {
144+
return r.errorNotFound([JsonApiError(detail: 'Resource not found')]);
140145
}
141-
final updated = dao[r.route.type].update(r.route.id, resource);
146+
final updated = dao[r.type].update(r.id, r.resource);
142147
if (updated == null) {
143-
return r.noContent();
148+
return r.sendNoContent();
144149
}
145-
return r.updated(updated);
150+
return r.sendUpdated(updated);
146151
}
147152

148153
@override
149-
Future replaceRelationship(ReplaceRelationship r) async {
150-
if (!dao.containsKey(r.route.type)) {
151-
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
154+
Future replaceToOne(ReplaceToOneRequest r) async {
155+
if (!dao.containsKey(r.type)) {
156+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
152157
}
153-
final rel = await r.relationshipData();
154-
if (rel is ToOne) {
155-
dao[r.route.type]
156-
.replaceToOne(r.route.id, r.route.relationship, rel.toIdentifier());
157-
return r.noContent();
158-
}
159-
if (rel is ToMany) {
160-
dao[r.route.type]
161-
.replaceToMany(r.route.id, r.route.relationship, rel.identifiers);
162-
return r.noContent();
158+
dao[r.type].replaceToOne(r.id, r.relationship, r.identifier);
159+
return r.sendNoContent();
160+
}
161+
162+
@override
163+
Future replaceToMany(ReplaceToManyRequest r) async {
164+
if (!dao.containsKey(r.type)) {
165+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
163166
}
167+
dao[r.type].replaceToMany(r.id, r.relationship, r.identifiers);
168+
return r.sendNoContent();
164169
}
165170

166171
@override
167-
Future addToRelationship(AddToRelationship r) async {
168-
if (!dao.containsKey(r.route.type)) {
169-
return r.notFound([JsonApiError(detail: 'Unknown resource type')]);
172+
Future addToMany(AddToManyRequest r) async {
173+
if (!dao.containsKey(r.type)) {
174+
return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
170175
}
171-
final result = dao[r.route.type]
172-
.addToMany(r.route.id, r.route.relationship, await r.identifiers());
173-
return r.toMany(result);
176+
final result = dao[r.type].addToMany(r.id, r.relationship, r.identifiers);
177+
return r.sendToMany(result);
174178
}
175179
}

lib/document.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
export 'package:json_api/src/document/document.dart';
22
export 'package:json_api/src/document/error.dart';
33
export 'package:json_api/src/document/identifier.dart';
4-
export 'package:json_api/src/document/identifier_json.dart';
4+
export 'package:json_api/src/document/identifier_object.dart';
55
export 'package:json_api/src/document/link.dart';
66
export 'package:json_api/src/document/primary_data.dart';
77
export 'package:json_api/src/document/relationship.dart';
88
export 'package:json_api/src/document/resource.dart';
9-
export 'package:json_api/src/document/resource_json.dart';
9+
export 'package:json_api/src/document/resource_collection_data.dart';
10+
export 'package:json_api/src/document/resource_data.dart';
11+
export 'package:json_api/src/document/resource_object.dart';

0 commit comments

Comments
 (0)