From bfe5a3dfad67491e2ebad8c9634ee78a9112d328 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 16 Nov 2016 15:26:14 -0500 Subject: [PATCH 01/36] Fixing contentType behavior for Response --- lib/http/http_controller.dart | 5 +- lib/http/http_response_exception.dart | 3 +- lib/http/request.dart | 39 ++------- lib/http/request_controller.dart | 4 +- lib/http/response.dart | 35 ++++++++- test/base/controller_test.dart | 32 +++++++- test/base/request_controller_test.dart | 105 +++++++++++++++++++++++++ 7 files changed, 183 insertions(+), 40 deletions(-) diff --git a/lib/http/http_controller.dart b/lib/http/http_controller.dart index 98ce47f11..6d77ab839 100644 --- a/lib/http/http_controller.dart +++ b/lib/http/http_controller.dart @@ -149,7 +149,10 @@ abstract class HTTPController extends RequestController { .reflectee as Future; var response = await eventualResponse; - response.headers[HttpHeaders.CONTENT_TYPE] = responseContentType; + if (response._contentType == null) { + response.contentType = responseContentType; + } + willSendResponse(response); return response; diff --git a/lib/http/http_response_exception.dart b/lib/http/http_response_exception.dart index 4bedffa10..8365664f5 100644 --- a/lib/http/http_response_exception.dart +++ b/lib/http/http_response_exception.dart @@ -18,6 +18,5 @@ class HTTPResponseException implements Exception { final int statusCode; /// A [Response] object derived from this exception. - Response get response => new Response(statusCode, - {HttpHeaders.CONTENT_TYPE: ContentType.JSON}, {"error": message}); + Response get response => new Response(statusCode, null, {"error": message})..contentType = ContentType.JSON; } diff --git a/lib/http/request.dart b/lib/http/request.dart index 9c5aa9fd9..742c0f999 100644 --- a/lib/http/request.dart +++ b/lib/http/request.dart @@ -135,51 +135,24 @@ class Request implements RequestControllerEvent { void respond(Response responseObject) { respondDate = new DateTime.now().toUtc(); + var encodedBody = responseObject.encodedBody; + response.statusCode = responseObject.statusCode; if (responseObject.headers != null) { responseObject.headers.forEach((k, v) { - if (v is ContentType) { - response.headers.add(HttpHeaders.CONTENT_TYPE, v.toString()); - } else { - response.headers.add(k, v); - } + response.headers.add(k, v); }); } - if (responseObject.body != null) { - _encodeBody(responseObject); + if (encodedBody != null) { + response.headers.add(HttpHeaders.CONTENT_TYPE, responseObject.contentType.toString()); + response.write(encodedBody); } response.close(); } - void _encodeBody(Response respObj) { - var contentTypeValue = respObj.headers["Content-Type"]; - if (contentTypeValue == null) { - contentTypeValue = ContentType.JSON; - response.headers.contentType = ContentType.JSON; - } else if (contentTypeValue is String) { - contentTypeValue = ContentType.parse(contentTypeValue); - } - - ContentType contentType = contentTypeValue; - var topLevel = Response._encoders[contentType.primaryType]; - if (topLevel == null) { - throw new RequestException( - "No encoder for $contentTypeValue, add with Request.addEncoder()."); - } - - var encoder = topLevel[contentType.subType]; - if (encoder == null) { - throw new RequestException( - "No encoder for $contentTypeValue, add with Request.addEncoder()."); - } - - var encodedValue = encoder(respObj.body); - response.write(encodedValue); - } - String toString() { return "${innerRequest.method} ${this.innerRequest.uri} (${this.receivedDate.millisecondsSinceEpoch})"; } diff --git a/lib/http/request_controller.dart b/lib/http/request_controller.dart index da7506b66..346825b9a 100644 --- a/lib/http/request_controller.dart +++ b/lib/http/request_controller.dart @@ -223,8 +223,8 @@ class RequestController extends Object with APIDocumentable { }; } - var response = new Response.serverError( - headers: {HttpHeaders.CONTENT_TYPE: ContentType.JSON}, body: body); + var response = new Response.serverError(body: body) + ..contentType = ContentType.JSON; _applyCORSHeadersIfNecessary(request, response); request.respond(response); diff --git a/lib/http/response.dart b/lib/http/response.dart index 946afc008..f42813aa6 100644 --- a/lib/http/response.dart +++ b/lib/http/response.dart @@ -59,14 +59,47 @@ class Response implements RequestControllerEvent { dynamic _body; + /// Returns the encoded [body] according to [contentType]. + /// + /// If there is no [body] present, this property is null. This property will use the encoders available through [addEncoder]. If + /// no encoder is found, [toString] is called on the body. + dynamic get encodedBody { + if (_body == null) { + return null; + } + + var encoder = null; + var topLevel = _encoders[contentType.primaryType]; + if (topLevel != null) { + encoder = topLevel[contentType.subType]; + } + + encoder ??= (Object value) => value.toString(); + + return encoder(_body); + } + /// Map of headers to send in this response. /// - /// Where the key is the Header name and value is the Header value. + /// Where the key is the Header name and value is the Header value. Values may be any type and by default will have [toString] invoked + /// on them. For [DateTime] values, the value will be converted into an HTTP date format. For [List] values, each value will be + /// have [toString] invoked on it and the resulting outputs will be joined together with the "," character. + /// + /// Adding a Content-Type header through this property has no effect. Use [contentType] instead. Map headers; /// The HTTP status code of this response. int statusCode; + /// The content type of the body of this response. + /// + /// Defaults to [ContentType.JSON]. This response's will be encoded according to this value prior to the response being sent. + ContentType get contentType => _contentType ?? ContentType.JSON; + void set contentType(ContentType t) { + _contentType = t; + } + ContentType _contentType; + /// The default constructor. /// /// There exist convenience constructors for common response status codes diff --git a/test/base/controller_test.dart b/test/base/controller_test.dart index 4287a8990..93d1c83c4 100644 --- a/test/base/controller_test.dart +++ b/test/base/controller_test.dart @@ -11,7 +11,6 @@ import '../helpers.dart'; void main() { HttpServer server; - ManagedDataModel dm = new ManagedDataModel([TestModel]); ManagedContext _ = new ManagedContext(dm, new DefaultPersistentStore()); @@ -259,6 +258,22 @@ void main() { expect(resp.body, '"false"'); }); + test("Content-Type can be set adjusting responseContentType", () async { + server = await enableController("/a", ContentTypeController); + var resp = await http.get("http://localhost:4040/a?opt=responseContentType"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "foo/bar"); + expect(resp.body, "body"); + }); + + test("Content-Type set directly on Response overrides responseContentType", () async { + server = await enableController("/a", ContentTypeController); + var resp = await http.get("http://localhost:4040/a?opt=direct"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "a/b"); + expect(resp.body, "body"); + }); + group("Annotated HTTP parameters", () { test("are supplied correctly", () async { server = await enableController("/a", HTTPParameterController); @@ -421,6 +436,9 @@ class FilteringController extends HTTPController { } class TController extends HTTPController { + TController() { + + } @httpGet Future getAll() async { return new Response.ok("getAll"); @@ -578,6 +596,18 @@ class ModelEncodeController extends HTTPController { } } +class ContentTypeController extends HTTPController { + @httpGet getThing(@HTTPQuery("opt") String opt) async { + if (opt == "responseContentType") { + responseContentType = new ContentType("foo", "bar"); + return new Response.ok("body"); + } else if (opt == "direct") { + return new Response.ok("body") + ..contentType = new ContentType("a", "b"); + } + } +} + Future enableController(String pattern, Type controller) async { var router = new Router(); router.route(pattern).generate( diff --git a/test/base/request_controller_test.dart b/test/base/request_controller_test.dart index 7c257a3ab..5f89f0049 100644 --- a/test/base/request_controller_test.dart +++ b/test/base/request_controller_test.dart @@ -147,8 +147,113 @@ void main() { expect(resp.headers["content-type"], startsWith("application/json")); expect(JSON.decode(resp.body), {"name": "Bob"}); }); + + test("Responding to request with no content-type, but does have a body, defaults to application/json", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server + .map((req) => new Request(req)) + .listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok({"a" : "b"}); + }); + await next.receive(req); + }); + + var resp = await http.get("http://localhost:8080"); + expect(resp.headers["content-type"], startsWith("application/json")); + expect(JSON.decode(resp.body), {"a" : "b"}); + }); + + test("Responding to a request with no explicit content-type and has a body that cannot be encoded to JSON will throw 500", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server + .map((req) => new Request(req)) + .listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(new DateTime.now()); + }); + await next.receive(req); + }); + + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 500); + expect(resp.headers["content-type"], "text/plain; charset=utf-8"); + expect(resp.body, ""); + }); + + test("Responding to request with no explicit content-type, but does not have a body, defaults to plaintext Content-Type header", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server + .map((req) => new Request(req)) + .listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(null); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(resp.headers["content-length"], "0"); + expect(resp.headers["content-type"], "text/plain; charset=utf-8"); + expect(resp.body, ""); + }); + + test("Using an encoder that doesn't exist, but for a known type, will yield the result of toString() instead of failing", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server + .map((req) => new Request(req)) + .listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok("")..contentType = new ContentType("text", "html", charset: "utf-8"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "text/html; charset=utf-8"); + expect(resp.body, ""); + }); + + test("Using an encoder that doesn't exist, for an unknown type, will yield the result of toString() instead of failing", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server + .map((req) => new Request(req)) + .listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(1234)..contentType = new ContentType("foo", "bar", charset: "utf-8"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "foo/bar; charset=utf-8"); + expect(resp.body, "1234"); + }); + + test("Using an encoder other than the default correctly encodes and sets content-type", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server + .map((req) => new Request(req)) + .listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(1234)..contentType = new ContentType("text", "plain"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "text/plain"); + expect(resp.body, "1234"); + }); } + class SomeObject implements HTTPSerializable { String name; From e8b8148eeac41338c6131c133cdf59f9d910b422 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Fri, 18 Nov 2016 20:08:37 -0500 Subject: [PATCH 02/36] Fix tests to use content-type --- test/utilities/test_client_test.dart | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/test/utilities/test_client_test.dart b/test/utilities/test_client_test.dart index 020c55c72..f0e0925ef 100644 --- a/test/utilities/test_client_test.dart +++ b/test/utilities/test_client_test.dart @@ -355,13 +355,14 @@ void main() { test("Can match text object", () async { var defaultTestClient = new TestClient.onPort(4000); - server.queueResponse( - new Response.ok("text", headers: {"Content-Type": "text/plain"})); + + server.queueResponse(new Response.ok("text")..contentType = ContentType.TEXT); var response = await defaultTestClient.request("/foo").get(); expect(response, hasBody("text")); - server.queueResponse( - new Response.ok("text", headers: {"Content-Type": "text/plain"})); + server.queueResponse(new Response.ok("text")..contentType = ContentType.TEXT); + + response = await defaultTestClient.request("/foo").get(); try { expect(response, hasBody("foobar")); @@ -375,13 +376,12 @@ void main() { test("Can match JSON Object", () async { var defaultTestClient = new TestClient.onPort(4000); - server.queueResponse(new Response.ok({"foo": "bar"}, - headers: {"Content-Type": "application/json"})); + + server.queueResponse(new Response.ok({"foo" : "bar"})..contentType = ContentType.JSON); var response = await defaultTestClient.request("/foo").get(); expect(response, hasBody(isNotNull)); - server.queueResponse(new Response.ok({"foo": "bar"}, - headers: {"Content-Type": "application/json"})); + server.queueResponse(new Response.ok({"foo" : "bar"})..contentType = ContentType.JSON); response = await defaultTestClient.request("/foo").get(); try { expect(response, hasBody({"foo": "notbar"})); @@ -391,9 +391,8 @@ void main() { expect(e.toString(), contains('Body: {"foo":"bar"}')); } - server.queueResponse(new Response.ok( - {"nocontenttype": "thatsaysthisisjson"}, - headers: {"Content-Type": "text/plain"})); + server.queueResponse(new Response.ok({"nocontenttype" : "thatsaysthisisjson"})..contentType = ContentType.TEXT); + response = await defaultTestClient.request("/foo").get(); try { expect(response, @@ -524,8 +523,8 @@ void main() { test("Omit status code ignores it", () async { var defaultTestClient = new TestClient.onPort(4000); - server.queueResponse(new Response.ok({"foo": "bar"}, - headers: {"content-type": "application/json"})); + server.queueResponse(new Response.ok({"foo" : "bar"})..contentType = ContentType.JSON); + var response = await defaultTestClient.request("/foo").get(); expect( response, From 7c67f075617d54c0d9faa006f7b7e32169125388 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Fri, 18 Nov 2016 21:19:19 -0500 Subject: [PATCH 03/36] Changing behavior for content type interpretation/handling for Response --- lib/http/response.dart | 29 ++++++++-- test/base/controller_test.dart | 16 ++++-- test/base/request_controller_test.dart | 76 ++++++++++++++++++++------ 3 files changed, 95 insertions(+), 26 deletions(-) diff --git a/lib/http/response.dart b/lib/http/response.dart index f42813aa6..fc0dae0bc 100644 --- a/lib/http/response.dart +++ b/lib/http/response.dart @@ -5,6 +5,12 @@ part of aqueduct; /// This object can be used to write an HTTP response and contains conveniences /// for creating these objects. class Response implements RequestControllerEvent { + /// The default value of a [contentType]. + /// + /// If no [contentType] is set for an instance, this is the value used. By default, this value is + /// [ContentType.JSON]. + static ContentType defaultContentType = ContentType.JSON; + /// Adds an HTTP Response Body encoder to list of available encoders for all [Request]s. /// /// By default, 'application/json' and 'text/plain' are implemented. If you wish to add another encoder @@ -27,7 +33,9 @@ class Response implements RequestControllerEvent { "application": { "json": (v) => JSON.encode(v), }, - "text": {"plain": (Object v) => v.toString()} + "text" : { + "*" : (Object v) => v.toString() + } }; /// An object representing the body of the [Response], which will be encoded when used to [Request.respond]. @@ -71,10 +79,12 @@ class Response implements RequestControllerEvent { var encoder = null; var topLevel = _encoders[contentType.primaryType]; if (topLevel != null) { - encoder = topLevel[contentType.subType]; + encoder = topLevel[contentType.subType] ?? topLevel["*"]; } - encoder ??= (Object value) => value.toString(); + if (encoder == null) { + throw new HTTPResponseException(500, "Could not encode body as ${contentType.toString()}."); + } return encoder(_body); } @@ -93,13 +103,22 @@ class Response implements RequestControllerEvent { /// The content type of the body of this response. /// - /// Defaults to [ContentType.JSON]. This response's will be encoded according to this value prior to the response being sent. - ContentType get contentType => _contentType ?? ContentType.JSON; + /// Defaults to [defaultContentType]. This response's body will be encoded according to this value. + /// The Content-Type header of the HTTP response will always be set according to this value. + ContentType get contentType => _contentType ?? defaultContentType; void set contentType(ContentType t) { _contentType = t; } ContentType _contentType; + /// Whether or nor this instance has explicitly has its [contentType] property. + /// + /// This value indicates whether or not [contentType] has been set, or is still using its default value. + /// + /// Some [RequestController]s might provide a value for this instance's Content-Type. For example, + /// an [HTTPController] has a [HTTPController.responseContentType] that it applies + bool get hasExplicitlySetContentType => _contentType != null; + /// The default constructor. /// /// There exist convenience constructors for common response status codes diff --git a/test/base/controller_test.dart b/test/base/controller_test.dart index 93d1c83c4..fa5f43cd3 100644 --- a/test/base/controller_test.dart +++ b/test/base/controller_test.dart @@ -258,11 +258,19 @@ void main() { expect(resp.body, '"false"'); }); + test("Content-Type defaults to application/json", () async { + server = await enableController("/a", TController); + var resp = await http.get("http://localhost:4040/a?param"); + expect(resp.statusCode, 200); + expect(ContentType.parse(resp.headers["content-type"]).primaryType, "application"); + expect(ContentType.parse(resp.headers["content-type"]).subType, "json"); + }); + test("Content-Type can be set adjusting responseContentType", () async { server = await enableController("/a", ContentTypeController); var resp = await http.get("http://localhost:4040/a?opt=responseContentType"); expect(resp.statusCode, 200); - expect(resp.headers["content-type"], "foo/bar"); + expect(resp.headers["content-type"], "text/plain"); expect(resp.body, "body"); }); @@ -270,7 +278,7 @@ void main() { server = await enableController("/a", ContentTypeController); var resp = await http.get("http://localhost:4040/a?opt=direct"); expect(resp.statusCode, 200); - expect(resp.headers["content-type"], "a/b"); + expect(resp.headers["content-type"], "text/plain"); expect(resp.body, "body"); }); @@ -599,11 +607,11 @@ class ModelEncodeController extends HTTPController { class ContentTypeController extends HTTPController { @httpGet getThing(@HTTPQuery("opt") String opt) async { if (opt == "responseContentType") { - responseContentType = new ContentType("foo", "bar"); + responseContentType = new ContentType("text", "plain"); return new Response.ok("body"); } else if (opt == "direct") { return new Response.ok("body") - ..contentType = new ContentType("a", "b"); + ..contentType = new ContentType("text", "plain"); } } } diff --git a/test/base/request_controller_test.dart b/test/base/request_controller_test.dart index 5f89f0049..070480c85 100644 --- a/test/base/request_controller_test.dart +++ b/test/base/request_controller_test.dart @@ -201,55 +201,97 @@ void main() { expect(resp.body, ""); }); - test("Using an encoder that doesn't exist, but for a known type, will yield the result of toString() instead of failing", () async { + test("Using an encoder that doesn't exist returns a 500", () async { server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); server .map((req) => new Request(req)) .listen((req) async { var next = new RequestController(); next.listen((req) async { - return new Response.ok("")..contentType = new ContentType("text", "html", charset: "utf-8"); + return new Response.ok(1234)..contentType = new ContentType("foo", "bar", charset: "utf-8"); }); await next.receive(req); }); var resp = await http.get("http://localhost:8080"); - expect(resp.statusCode, 200); - expect(resp.headers["content-type"], "text/html; charset=utf-8"); - expect(resp.body, ""); + var contentType = ContentType.parse(resp.headers["content-type"]); + expect(resp.statusCode, 500); + expect(contentType.primaryType, "application"); + expect(contentType.subType, "json"); + expect(JSON.decode(resp.body), {"error" : "Could not encode body as foo/bar; charset=utf-8."}); }); - test("Using an encoder that doesn't exist, for an unknown type, will yield the result of toString() instead of failing", () async { + test("Using an encoder other than the default correctly encodes and sets content-type", () async { server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); server .map((req) => new Request(req)) .listen((req) async { - var next = new RequestController(); - next.listen((req) async { - return new Response.ok(1234)..contentType = new ContentType("foo", "bar", charset: "utf-8"); - }); - await next.receive(req); - }); + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(1234)..contentType = new ContentType("text", "plain"); + }); + await next.receive(req); + }); var resp = await http.get("http://localhost:8080"); expect(resp.statusCode, 200); - expect(resp.headers["content-type"], "foo/bar; charset=utf-8"); + expect(resp.headers["content-type"], "text/plain"); expect(resp.body, "1234"); }); - test("Using an encoder other than the default correctly encodes and sets content-type", () async { + test("A decoder with a match-all subtype will be used when matching", () async { + Response.addEncoder(new ContentType("b", "*"), (s) => s); server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); server .map((req) => new Request(req)) .listen((req) async { var next = new RequestController(); next.listen((req) async { - return new Response.ok(1234)..contentType = new ContentType("text", "plain"); + return new Response.ok("hello")..contentType = new ContentType("b", "bar", charset: "utf-8"); }); await next.receive(req); }); var resp = await http.get("http://localhost:8080"); expect(resp.statusCode, 200); - expect(resp.headers["content-type"], "text/plain"); - expect(resp.body, "1234"); + expect(resp.headers["content-type"], "b/bar; charset=utf-8"); + expect(resp.body, "hello"); + }); + + test("A decoder with a subtype always trumps a decoder that matches any subtype", () async { + Response.addEncoder(new ContentType("a", "*"), (s) => s); + Response.addEncoder(new ContentType("a", "html"), (s) { + return "$s"; + }); + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server + .map((req) => new Request(req)) + .listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok("hello")..contentType = new ContentType("a", "html", charset: "utf-8"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "a/html; charset=utf-8"); + expect(resp.body, "hello"); + }); + + test("Using an encoder that blows up during encoded returns 500 safely", () async { + Response.addEncoder(new ContentType("foo", "bar"), (s) { + throw new Exception("uhoh"); + }); + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server + .map((req) => new Request(req)) + .listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok("hello")..contentType = new ContentType("foo", "bar", charset: "utf-8"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 500); }); } From 3411ee71e4148528fabb3fcc98ddf8d47e439554 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Fri, 18 Nov 2016 21:29:31 -0500 Subject: [PATCH 04/36] Fixed documentation --- lib/http/response.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/http/response.dart b/lib/http/response.dart index fc0dae0bc..93931436c 100644 --- a/lib/http/response.dart +++ b/lib/http/response.dart @@ -13,8 +13,16 @@ class Response implements RequestControllerEvent { /// Adds an HTTP Response Body encoder to list of available encoders for all [Request]s. /// - /// By default, 'application/json' and 'text/plain' are implemented. If you wish to add another encoder - /// to your application, use this method. The [encoder] must take one argument of any type, and return a value + /// When the [contentType] of an instance is set, an encoder function is applied to the data. This method + /// adds an encoder function for [type]. + /// + /// By default, 'application/json' and 'text/*' are available. A [Response] with "application/json" [contentType] + /// will be encoded by invoking [JSON.decode] on the instance's [body]. The default encoder for [ContentType]s whose primary type is "text" will invoke [toString] + /// on the instance's [body]. + /// + /// [type] can have a '*' [ContentType.subType] that matches all subtypes for a primary type. + /// + /// An [encoder] must take one argument of any type, and return a value /// that will become the HTTP response body. /// /// The return value is written to the response with [IOSink.write] and so it must either be a [String] or its [toString] From 3960853723509eb44f569717843a03a865982adb Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Fri, 18 Nov 2016 21:30:45 -0500 Subject: [PATCH 05/36] Fixed docs, fixed style thing --- lib/http/http_controller.dart | 2 +- lib/http/response.dart | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/http/http_controller.dart b/lib/http/http_controller.dart index 6d77ab839..a726f39d2 100644 --- a/lib/http/http_controller.dart +++ b/lib/http/http_controller.dart @@ -149,7 +149,7 @@ abstract class HTTPController extends RequestController { .reflectee as Future; var response = await eventualResponse; - if (response._contentType == null) { + if (!response.hasExplicitlySetContentType) { response.contentType = responseContentType; } diff --git a/lib/http/response.dart b/lib/http/response.dart index 93931436c..90f7549b9 100644 --- a/lib/http/response.dart +++ b/lib/http/response.dart @@ -122,9 +122,6 @@ class Response implements RequestControllerEvent { /// Whether or nor this instance has explicitly has its [contentType] property. /// /// This value indicates whether or not [contentType] has been set, or is still using its default value. - /// - /// Some [RequestController]s might provide a value for this instance's Content-Type. For example, - /// an [HTTPController] has a [HTTPController.responseContentType] that it applies bool get hasExplicitlySetContentType => _contentType != null; /// The default constructor. From 46948eb7317805ddfa7a3971e832b8fa36daf073 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 22 Nov 2016 14:25:38 -0500 Subject: [PATCH 06/36] dartfmt --- lib/http/http_response_exception.dart | 3 ++- lib/http/request.dart | 3 ++- lib/http/response.dart | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/http/http_response_exception.dart b/lib/http/http_response_exception.dart index 8365664f5..1ce0905a2 100644 --- a/lib/http/http_response_exception.dart +++ b/lib/http/http_response_exception.dart @@ -18,5 +18,6 @@ class HTTPResponseException implements Exception { final int statusCode; /// A [Response] object derived from this exception. - Response get response => new Response(statusCode, null, {"error": message})..contentType = ContentType.JSON; + Response get response => new Response(statusCode, null, {"error": message}) + ..contentType = ContentType.JSON; } diff --git a/lib/http/request.dart b/lib/http/request.dart index 742c0f999..76891882a 100644 --- a/lib/http/request.dart +++ b/lib/http/request.dart @@ -146,7 +146,8 @@ class Request implements RequestControllerEvent { } if (encodedBody != null) { - response.headers.add(HttpHeaders.CONTENT_TYPE, responseObject.contentType.toString()); + response.headers + .add(HttpHeaders.CONTENT_TYPE, responseObject.contentType.toString()); response.write(encodedBody); } diff --git a/lib/http/response.dart b/lib/http/response.dart index 90f7549b9..a856a78c6 100644 --- a/lib/http/response.dart +++ b/lib/http/response.dart @@ -41,9 +41,7 @@ class Response implements RequestControllerEvent { "application": { "json": (v) => JSON.encode(v), }, - "text" : { - "*" : (Object v) => v.toString() - } + "text": {"*": (Object v) => v.toString()} }; /// An object representing the body of the [Response], which will be encoded when used to [Request.respond]. @@ -91,7 +89,8 @@ class Response implements RequestControllerEvent { } if (encoder == null) { - throw new HTTPResponseException(500, "Could not encode body as ${contentType.toString()}."); + throw new HTTPResponseException( + 500, "Could not encode body as ${contentType.toString()}."); } return encoder(_body); @@ -117,6 +116,7 @@ class Response implements RequestControllerEvent { void set contentType(ContentType t) { _contentType = t; } + ContentType _contentType; /// Whether or nor this instance has explicitly has its [contentType] property. From f8394662247251c86e00695849bef02e1e991476 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 22 Nov 2016 14:30:16 -0500 Subject: [PATCH 07/36] Bump version number, change log --- CHANGELOG.md | 3 +++ lib/http/http_controller.dart | 7 +++---- pubspec.yaml | 2 +- test/base/controller_test.dart | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72df4882b..2ee598377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # aqueduct changelog +## 1.0.4 +- Added new `Response.contentType` property. Adding "Content-Type" to the headers of a `Response` no longer has any effect; use this property instead. + ## 1.0.3 - Fix to allow Windows user to use `aqueduct setup`. - Fix to CORS processing. diff --git a/lib/http/http_controller.dart b/lib/http/http_controller.dart index a726f39d2..18583b27c 100644 --- a/lib/http/http_controller.dart +++ b/lib/http/http_controller.dart @@ -46,11 +46,10 @@ abstract class HTTPController extends RequestController { _applicationWWWFormURLEncodedContentType ]; - /// The content type of responses from this [HTTPController]. + /// The default content type of responses from this [HTTPController]. /// - /// This type will automatically be written to this response's - /// HTTP header. Defaults to "application/json". This value determines how the body data returned from this controller - /// in a [Response] is encoded. + /// If the [Response.contentType] has not explicitly been set by a responder method in this controller, the controller will set + /// that property with this value. Defaults to "application/json". ContentType responseContentType = ContentType.JSON; /// The HTTP request body object, after being decoded. diff --git a/pubspec.yaml b/pubspec.yaml index 045845346..312e497ed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: aqueduct -version: 1.0.3 +version: 1.0.4 description: A fully featured server-side framework built for productivity and testability. author: stable|kernel homepage: https://github.com/stablekernel/aqueduct diff --git a/test/base/controller_test.dart b/test/base/controller_test.dart index fa5f43cd3..49e91c0c6 100644 --- a/test/base/controller_test.dart +++ b/test/base/controller_test.dart @@ -260,7 +260,7 @@ void main() { test("Content-Type defaults to application/json", () async { server = await enableController("/a", TController); - var resp = await http.get("http://localhost:4040/a?param"); + var resp = await http.get("http://localhost:4040/a"); expect(resp.statusCode, 200); expect(ContentType.parse(resp.headers["content-type"]).primaryType, "application"); expect(ContentType.parse(resp.headers["content-type"]).subType, "json"); From 8f86c7c7057693e2c7963dd192a4a0a17eb9d186 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 22 Nov 2016 23:39:38 -0500 Subject: [PATCH 08/36] WIP; clean up private/public --- lib/aqueduct.dart | 91 +------ .../persistent_store_query.dart | 230 ------------------ lib/{ => src}/application/application.dart | 18 +- .../application_configuration.dart | 5 +- lib/{ => src}/application/isolate_server.dart | 25 +- .../application/isolate_supervisor.dart | 13 +- lib/src/auth/auth.dart | 7 + lib/{ => src}/auth/auth_code_controller.dart | 6 +- lib/{ => src}/auth/auth_controller.dart | 6 +- lib/{ => src}/auth/authentication_server.dart | 14 +- lib/{ => src}/auth/authorization_parser.dart | 4 +- lib/{ => src}/auth/authorizer.dart | 6 +- lib/{ => src}/auth/client.dart | 2 +- lib/{ => src}/auth/protocols.dart | 5 +- lib/{ => src}/commands/cli_command.dart | 8 +- lib/{ => src}/commands/migration_runner.dart | 14 +- lib/{ => src}/commands/setup_command.dart | 7 +- lib/{ => src}/commands/template_creator.dart | 9 +- lib/src/db/db.dart | 5 + lib/{ => src}/db/managed/attributes.dart | 2 +- lib/{ => src}/db/managed/backing.dart | 26 +- lib/{ => src}/db/managed/context.dart | 23 +- lib/{ => src}/db/managed/data_model.dart | 11 +- .../db/managed/data_model_builder.dart | 31 +-- lib/{ => src}/db/managed/entity.dart | 23 +- lib/src/db/managed/managed.dart | 8 + lib/{ => src}/db/managed/object.dart | 37 ++- .../db/managed/property_description.dart | 3 +- lib/src/db/managed/query_matchable.dart | 24 ++ lib/{ => src}/db/managed/set.dart | 14 +- .../db/persistent_store/persistent_store.dart | 7 +- .../persistent_store_query.dart | 71 ++++++ .../postgresql_persistent_store.dart | 33 ++- .../postgresql_schema_generator.dart | 21 +- .../db/query/matcher_expression.dart | 68 ++---- lib/src/db/query/matcher_internal.dart | 37 +++ lib/{ => src}/db/query/page.dart | 2 +- lib/{ => src}/db/query/predicate.dart | 21 +- lib/{ => src}/db/query/query.dart | 92 ++++--- lib/src/db/query/query_mapping.dart | 132 ++++++++++ lib/{ => src}/db/query/sort_descriptor.dart | 2 +- lib/{ => src}/db/schema/migration.dart | 37 ++- lib/{ => src}/db/schema/schema.dart | 11 +- lib/{ => src}/db/schema/schema_builder.dart | 17 +- lib/{ => src}/db/schema/schema_column.dart | 7 +- lib/{ => src}/db/schema/schema_table.dart | 5 +- lib/{ => src}/http/body_decoder.dart | 4 +- lib/{ => src}/http/controller_routing.dart | 30 ++- lib/{ => src}/http/cors_policy.dart | 3 +- lib/{ => src}/http/documentable.dart | 18 +- lib/src/http/http.dart | 15 ++ lib/{ => src}/http/http_controller.dart | 94 ++++--- .../http/http_controller_internal.dart} | 179 +++++++------- .../http/http_response_exception.dart | 6 +- lib/{ => src}/http/query_controller.dart | 6 +- lib/{ => src}/http/request.dart | 8 +- lib/{ => src}/http/request_controller.dart | 20 +- lib/{ => src}/http/request_path.dart | 2 +- lib/{ => src}/http/request_sink.dart | 9 +- lib/{ => src}/http/resource_controller.dart | 7 +- lib/{ => src}/http/response.dart | 4 +- lib/{ => src}/http/route_node.dart | 22 +- lib/{ => src}/http/router.dart | 11 +- lib/{ => src}/http/serializable.dart | 2 +- lib/{ => src}/utilities/mirror_helpers.dart | 4 +- lib/{ => src}/utilities/mock_server.dart | 7 +- lib/{ => src}/utilities/pbkdf2.dart | 6 +- lib/{ => src}/utilities/source_generator.dart | 18 +- lib/{ => src}/utilities/test_client.dart | 7 +- lib/{ => src}/utilities/test_matchers.dart | 3 +- lib/{ => src}/utilities/token_generator.dart | 0 test/auth/auth_controller_test.dart | 2 + test/helpers.dart | 2 + 73 files changed, 979 insertions(+), 750 deletions(-) delete mode 100644 lib/db/persistent_store/persistent_store_query.dart rename lib/{ => src}/application/application.dart (93%) rename lib/{ => src}/application/application_configuration.dart (96%) rename lib/{ => src}/application/isolate_server.dart (81%) rename lib/{ => src}/application/isolate_supervisor.dart (92%) create mode 100644 lib/src/auth/auth.dart rename lib/{ => src}/auth/auth_code_controller.dart (97%) rename lib/{ => src}/auth/auth_controller.dart (98%) rename lib/{ => src}/auth/authentication_server.dart (98%) rename lib/{ => src}/auth/authorization_parser.dart (97%) rename lib/{ => src}/auth/authorizer.dart (97%) rename lib/{ => src}/auth/client.dart (97%) rename lib/{ => src}/auth/protocols.dart (98%) rename lib/{ => src}/commands/cli_command.dart (86%) rename lib/{ => src}/commands/migration_runner.dart (95%) rename lib/{ => src}/commands/setup_command.dart (97%) rename lib/{ => src}/commands/template_creator.dart (98%) create mode 100644 lib/src/db/db.dart rename lib/{ => src}/db/managed/attributes.dart (99%) rename lib/{ => src}/db/managed/backing.dart (83%) rename lib/{ => src}/db/managed/context.dart (90%) rename lib/{ => src}/db/managed/data_model.dart (90%) rename lib/{ => src}/db/managed/data_model_builder.dart (92%) rename lib/{ => src}/db/managed/entity.dart (93%) create mode 100644 lib/src/db/managed/managed.dart rename lib/{ => src}/db/managed/object.dart (90%) rename lib/{ => src}/db/managed/property_description.dart (99%) create mode 100644 lib/src/db/managed/query_matchable.dart rename lib/{ => src}/db/managed/set.dart (91%) rename lib/{ => src}/db/persistent_store/persistent_store.dart (94%) create mode 100644 lib/src/db/persistent_store/persistent_store_query.dart rename lib/{ => src}/db/postgresql/postgresql_persistent_store.dart (94%) rename lib/{ => src}/db/postgresql/postgresql_schema_generator.dart (93%) rename lib/{ => src}/db/query/matcher_expression.dart (72%) create mode 100644 lib/src/db/query/matcher_internal.dart rename lib/{ => src}/db/query/page.dart (99%) rename lib/{ => src}/db/query/predicate.dart (86%) rename lib/{ => src}/db/query/query.dart (84%) create mode 100644 lib/src/db/query/query_mapping.dart rename lib/{ => src}/db/query/sort_descriptor.dart (97%) rename lib/{ => src}/db/schema/migration.dart (90%) rename lib/{ => src}/db/schema/schema.dart (95%) rename lib/{ => src}/db/schema/schema_builder.dart (94%) rename lib/{ => src}/db/schema/schema_column.dart (98%) rename lib/{ => src}/db/schema/schema_table.dart (96%) rename lib/{ => src}/http/body_decoder.dart (98%) rename lib/{ => src}/http/controller_routing.dart (63%) rename lib/{ => src}/http/cors_policy.dart (99%) rename lib/{ => src}/http/documentable.dart (98%) create mode 100644 lib/src/http/http.dart rename lib/{ => src}/http/http_controller.dart (83%) rename lib/{http/parameter_matching.dart => src/http/http_controller_internal.dart} (54%) rename lib/{ => src}/http/http_response_exception.dart (89%) rename lib/{ => src}/http/query_controller.dart (97%) rename lib/{ => src}/http/request.dart (98%) rename lib/{ => src}/http/request_controller.dart (96%) rename lib/{ => src}/http/request_path.dart (99%) rename lib/{ => src}/http/request_sink.dart (97%) rename lib/{ => src}/http/resource_controller.dart (99%) rename lib/{ => src}/http/response.dart (99%) rename lib/{ => src}/http/route_node.dart (85%) rename lib/{ => src}/http/router.dart (96%) rename lib/{ => src}/http/serializable.dart (96%) rename lib/{ => src}/utilities/mirror_helpers.dart (90%) rename lib/{ => src}/utilities/mock_server.dart (98%) rename lib/{ => src}/utilities/pbkdf2.dart (97%) rename lib/{ => src}/utilities/source_generator.dart (89%) rename lib/{ => src}/utilities/test_client.dart (98%) rename lib/{ => src}/utilities/test_matchers.dart (99%) rename lib/{ => src}/utilities/token_generator.dart (100%) diff --git a/lib/aqueduct.dart b/lib/aqueduct.dart index 9108a013c..a3680a27e 100644 --- a/lib/aqueduct.dart +++ b/lib/aqueduct.dart @@ -26,88 +26,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND /// application: Classes in this module begin with 'Application' and are responsible for starting and stopping web servers on a number of isolates. library aqueduct; -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:math'; -import 'dart:mirrors'; - -import 'package:meta/meta.dart'; -import 'package:analyzer/analyzer.dart'; -import 'package:args/args.dart'; -import 'package:crypto/crypto.dart'; -import 'package:logging/logging.dart'; -import 'package:matcher/matcher.dart'; -import 'package:postgres/postgres.dart'; -import 'package:safe_config/safe_config.dart'; -import 'package:yaml/yaml.dart'; -import 'package:path/path.dart' as path_lib; - -import 'utilities/mirror_helpers.dart'; -import 'utilities/token_generator.dart'; - export 'package:logging/logging.dart'; export 'package:safe_config/safe_config.dart'; -part 'application/application.dart'; -part 'application/application_configuration.dart'; -part 'application/isolate_server.dart'; -part 'application/isolate_supervisor.dart'; -part 'auth/auth_code_controller.dart'; -part 'auth/auth_controller.dart'; -part 'auth/authentication_server.dart'; -part 'auth/authorizer.dart'; -part 'auth/authorization_parser.dart'; -part 'auth/client.dart'; -part 'auth/protocols.dart'; -part 'commands/cli_command.dart'; -part 'commands/migration_runner.dart'; -part 'commands/setup_command.dart'; -part 'commands/template_creator.dart'; -part 'db/managed/attributes.dart'; -part 'db/managed/backing.dart'; -part 'db/managed/context.dart'; -part 'db/managed/data_model.dart'; -part 'db/managed/data_model_builder.dart'; -part 'db/managed/entity.dart'; -part 'db/managed/object.dart'; -part 'db/managed/property_description.dart'; -part 'db/managed/set.dart'; -part 'db/persistent_store/persistent_store.dart'; -part 'db/persistent_store/persistent_store_query.dart'; -part 'db/postgresql/postgresql_persistent_store.dart'; -part 'db/postgresql/postgresql_schema_generator.dart'; -part 'db/query/matcher_expression.dart'; -part 'db/query/page.dart'; -part 'db/query/predicate.dart'; -part 'db/query/query.dart'; -part 'db/query/sort_descriptor.dart'; -part 'db/schema/migration.dart'; -part 'db/schema/schema.dart'; -part 'db/schema/schema_builder.dart'; -part 'db/schema/schema_column.dart'; -part 'db/schema/schema_table.dart'; -part 'http/body_decoder.dart'; -part 'http/controller_routing.dart'; -part 'http/cors_policy.dart'; -part 'http/documentable.dart'; -part 'http/http_controller.dart'; -part 'http/http_response_exception.dart'; -part 'http/query_controller.dart'; -part 'http/parameter_matching.dart'; -part 'http/request.dart'; -part 'http/request_controller.dart'; -part 'http/request_path.dart'; -part 'http/request_sink.dart'; -part 'http/resource_controller.dart'; -part 'http/response.dart'; -part 'http/route_node.dart'; -part 'http/router.dart'; -part 'http/serializable.dart'; -part 'utilities/mock_server.dart'; -part 'utilities/pbkdf2.dart'; -part 'utilities/source_generator.dart'; -part 'utilities/test_client.dart'; -part 'utilities/test_matchers.dart'; +export 'src/application/application.dart'; +export 'src/auth/auth.dart'; +export 'src/commands/cli_command.dart'; +export 'src/db/db.dart'; +export 'src/http/http.dart'; +export 'src/utilities/mock_server.dart'; +export 'src/utilities/pbkdf2.dart'; +export 'src/utilities/test_client.dart'; +export 'src/utilities/test_matchers.dart'; diff --git a/lib/db/persistent_store/persistent_store_query.dart b/lib/db/persistent_store/persistent_store_query.dart deleted file mode 100644 index e2e09766e..000000000 --- a/lib/db/persistent_store/persistent_store_query.dart +++ /dev/null @@ -1,230 +0,0 @@ -part of aqueduct; - -/// This enumeration is used internaly. -enum PersistentJoinType { leftOuter } - -/// This class is used internally to map [Query] to something a [PersistentStore] can execute. -class PersistentStoreQuery { - PersistentStoreQuery(this.rootEntity, PersistentStore store, Query q) { - confirmQueryModifiesAllInstancesOnDeleteOrUpdate = - q.confirmQueryModifiesAllInstancesOnDeleteOrUpdate; - timeoutInSeconds = q.timeoutInSeconds; - sortDescriptors = q.sortDescriptors; - resultKeys = _mappingElementsForList( - (q.resultProperties ?? rootEntity.defaultProperties), rootEntity); - - if (q._matchOn != null) { - predicate = new QueryPredicate._fromQueryIncludable(q._matchOn, store); - } else { - predicate = q.predicate; - } - - if (q._matchOn?._hasJoinElements ?? false) { - var joinElements = _joinElementsFromQueryMatchable( - q.matchOn, store, q.nestedResultProperties); - resultKeys.addAll(joinElements); - - if (q.pageDescriptor != null) { - throw new QueryException(QueryExceptionEvent.requestFailure, - message: - "Query cannot have properties that are includeInResultSet and also have a pageDescriptor."); - } - } else { - fetchLimit = q.fetchLimit; - offset = q.offset; - - pageDescriptor = _validatePageDescriptor(q.pageDescriptor); - - values = _mappingElementsForMap( - (q.valueMap ?? q.values?.backingMap), rootEntity); - } - } - - int offset = 0; - int fetchLimit = 0; - int timeoutInSeconds = 30; - bool confirmQueryModifiesAllInstancesOnDeleteOrUpdate; - ManagedEntity rootEntity; - QueryPage pageDescriptor; - QueryPredicate predicate; - List sortDescriptors; - List values; - List resultKeys; - - static ManagedPropertyDescription _propertyForName( - ManagedEntity entity, String propertyName) { - var property = entity.properties[propertyName]; - if (property == null) { - throw new QueryException(QueryExceptionEvent.internalFailure, - message: - "Property $propertyName does not exist on ${entity.tableName}"); - } - if (property is ManagedRelationshipDescription && - property.relationshipType != ManagedRelationshipType.belongsTo) { - throw new QueryException(QueryExceptionEvent.internalFailure, - message: - "Property $propertyName is a hasMany or hasOne relationship and is invalid as a result property of ${entity.tableName}, use matchOn.$propertyName.includeInResultSet = true instead."); - } - - return property; - } - - static List _mappingElementsForList( - List keys, ManagedEntity entity) { - if (!keys.contains(entity.primaryKey)) { - keys.add(entity.primaryKey); - } - - return keys.map((key) { - var property = _propertyForName(entity, key); - return new PersistentColumnMapping(property, null); - }).toList(); - } - - QueryPage _validatePageDescriptor(QueryPage page) { - if (page == null) { - return null; - } - - var prop = rootEntity.attributes[page.propertyName]; - if (prop == null) { - throw new QueryException(QueryExceptionEvent.requestFailure, - message: - "Property ${page.propertyName} in pageDescriptor does not exist on ${rootEntity.tableName}."); - } - - if (page.boundingValue != null && - !prop.isAssignableWith(page.boundingValue)) { - throw new QueryException(QueryExceptionEvent.requestFailure, - message: - "Property ${page.propertyName} in pageDescriptor has invalid type (${page.boundingValue.runtimeType})."); - } - - return page; - } - - List _mappingElementsForMap( - Map valueMap, ManagedEntity entity) { - return valueMap?.keys - ?.map((key) { - var property = entity.properties[key]; - if (property == null) { - throw new QueryException(QueryExceptionEvent.requestFailure, - message: - "Property $key in values does not exist on ${entity.tableName}"); - } - - var value = valueMap[key]; - if (property is ManagedRelationshipDescription) { - if (property.relationshipType != - ManagedRelationshipType.belongsTo) { - return null; - } - - if (value != null) { - if (value is ManagedObject) { - value = value[property.destinationEntity.primaryKey]; - } else if (value is Map) { - value = value[property.destinationEntity.primaryKey]; - } else { - throw new QueryException(QueryExceptionEvent.internalFailure, - message: - "Property $key on ${entity.tableName} in Query values must be a Map or ${MirrorSystem.getName(property.destinationEntity.instanceType.simpleName)} "); - } - } - } - - return new PersistentColumnMapping(property, value); - }) - ?.where((m) => m != null) - ?.toList(); - } - - static List _joinElementsFromQueryMatchable( - _QueryMatchableExtension matcherBackedObject, - PersistentStore store, - Map> nestedResultProperties) { - var entity = matcherBackedObject.entity; - var propertiesToJoin = matcherBackedObject._joinPropertyKeys; - - return propertiesToJoin - .map((propertyName) { - _QueryMatchableExtension inner = - matcherBackedObject._matcherMap[propertyName]; - - var relDesc = entity.relationships[propertyName]; - var predicate = new QueryPredicate._fromQueryIncludable(inner, store); - var nestedProperties = - nestedResultProperties[inner.entity.instanceType.reflectedType]; - var propertiesToFetch = - nestedProperties ?? inner.entity.defaultProperties; - - var joinElements = [ - new PersistentJoinMapping( - PersistentJoinType.leftOuter, - relDesc, - predicate, - _mappingElementsForList(propertiesToFetch, inner.entity)) - ]; - - if (inner._hasJoinElements) { - joinElements.addAll(_joinElementsFromQueryMatchable( - inner, store, nestedResultProperties)); - } - - return joinElements; - }) - .expand((l) => l) - .toList(); - } -} - -/// This class is used internally. -class PersistentColumnMapping { - PersistentColumnMapping(this.property, this.value); - PersistentColumnMapping.fromElement( - PersistentColumnMapping original, this.value) { - property = original.property; - } - - ManagedPropertyDescription property; - dynamic value; - - String toString() { - return "MappingElement on $property (Value = $value)"; - } -} - -/// This class is used internally. -class PersistentJoinMapping extends PersistentColumnMapping { - PersistentJoinMapping(this.type, ManagedPropertyDescription property, - this.predicate, this.resultKeys) - : super(property, null) { - var primaryKeyElement = this.resultKeys.firstWhere((e) { - var eProp = e.property; - if (eProp is ManagedAttributeDescription) { - return eProp.isPrimaryKey; - } - return false; - }); - - primaryKeyIndex = this.resultKeys.indexOf(primaryKeyElement); - } - - PersistentJoinMapping.fromElement( - PersistentJoinMapping original, List values) - : super.fromElement(original, values) { - type = original.type; - primaryKeyIndex = original.primaryKeyIndex; - } - - PersistentJoinType type; - ManagedPropertyDescription get joinProperty => - (property as ManagedRelationshipDescription).inverseRelationship; - QueryPredicate predicate; - List resultKeys; - - int primaryKeyIndex; - List get values => - value as List; -} diff --git a/lib/application/application.dart b/lib/src/application/application.dart similarity index 93% rename from lib/application/application.dart rename to lib/src/application/application.dart index b771ba5ea..35a76512d 100644 --- a/lib/application/application.dart +++ b/lib/src/application/application.dart @@ -1,4 +1,18 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:mirrors'; + +import 'package:logging/logging.dart'; + +import '../http/http.dart'; +import 'application_configuration.dart'; +import 'isolate_server.dart'; +import 'isolate_supervisor.dart'; + +export 'application_configuration.dart'; +export 'isolate_server.dart'; +export 'isolate_supervisor.dart'; /// A container for web server applications. /// @@ -61,8 +75,6 @@ class Application { await server.start(); } else { - configuration._shared = true; - supervisors = []; try { for (int i = 0; i < numberOfInstances; i++) { diff --git a/lib/application/application_configuration.dart b/lib/src/application/application_configuration.dart similarity index 96% rename from lib/application/application_configuration.dart rename to lib/src/application/application_configuration.dart index 48aae4e57..5d9c1e508 100644 --- a/lib/application/application_configuration.dart +++ b/lib/src/application/application_configuration.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'dart:io'; +import 'application.dart'; /// A set of values to configure an instance of [Application]. class ApplicationConfiguration { @@ -36,6 +37,4 @@ class ApplicationConfiguration { /// Allows delivery of custom configuration parameters to [RequestSink] instances /// that are attached to this application. Map configurationOptions; - - bool _shared = false; } diff --git a/lib/application/isolate_server.dart b/lib/src/application/isolate_server.dart similarity index 81% rename from lib/application/isolate_server.dart rename to lib/src/application/isolate_server.dart index 05e9906b6..847f161b8 100644 --- a/lib/application/isolate_server.dart +++ b/lib/src/application/isolate_server.dart @@ -1,4 +1,15 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:mirrors'; + +import 'package:logging/logging.dart'; + +import '../http/request.dart'; +import '../http/request_sink.dart'; +import 'application.dart'; +import 'application_configuration.dart'; +import 'isolate_supervisor.dart'; /// Used internally. class ApplicationServer { @@ -12,7 +23,7 @@ class ApplicationServer { sink.server = this; } - Future start() async { + Future start({bool shareHttpServer: false}) async { try { sink.setupRouter(sink.router); sink.router?.finalize(); @@ -23,11 +34,11 @@ class ApplicationServer { configuration.port, configuration.securityContext, requestClientCertificate: configuration.isUsingClientCertificate, v6Only: configuration.isIpv6Only, - shared: configuration._shared); + shared: shareHttpServer); } else { server = await HttpServer.bind( configuration.address, configuration.port, - v6Only: configuration.isIpv6Only, shared: configuration._shared); + v6Only: configuration.isIpv6Only, shared: shareHttpServer); } server.autoCompress = true; @@ -79,10 +90,10 @@ class ApplicationIsolateServer extends ApplicationServer { } void listener(dynamic message) { - if (message == ApplicationIsolateSupervisor._MessageStop) { + if (message == ApplicationIsolateSupervisor.MessageStop) { server.close(force: true).then((s) { supervisingApplicationPort - .send(ApplicationIsolateSupervisor._MessageStop); + .send(ApplicationIsolateSupervisor.MessageStop); }); } } @@ -100,5 +111,5 @@ void isolateServerEntryPoint(ApplicationInitialServerMessage params) { var server = new ApplicationIsolateServer( app, params.configuration, params.identifier, params.parentMessagePort); - server.start(); + server.start(shareHttpServer: true); } diff --git a/lib/application/isolate_supervisor.dart b/lib/src/application/isolate_supervisor.dart similarity index 92% rename from lib/application/isolate_supervisor.dart rename to lib/src/application/isolate_supervisor.dart index 8bc6294c8..63648ed86 100644 --- a/lib/application/isolate_supervisor.dart +++ b/lib/src/application/isolate_supervisor.dart @@ -1,10 +1,15 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:isolate'; + +import 'package:logging/logging.dart'; + +import 'application.dart'; /// Represents the supervision of a [ApplicationIsolateServer]. /// /// You should not use this class directly. class ApplicationIsolateSupervisor { - static String _MessageStop = "_MessageStop"; + static String MessageStop = "_MessageStop"; /// Create an isntance of [ApplicationIsolateSupervisor]. ApplicationIsolateSupervisor(this.supervisingApplication, this.isolate, @@ -43,7 +48,7 @@ class ApplicationIsolateSupervisor { /// Stops the [Isolate] being supervised. Future stop() async { _stopCompleter = new Completer(); - _serverSendPort.send(_MessageStop); + _serverSendPort.send(MessageStop); await _stopCompleter.future.timeout(new Duration(seconds: 30)); isolate.kill(); @@ -55,7 +60,7 @@ class ApplicationIsolateSupervisor { _launchCompleter = null; _serverSendPort = message; - } else if (message == _MessageStop) { + } else if (message == MessageStop) { _stopCompleter?.complete(); _stopCompleter = null; } else if (message is List) { diff --git a/lib/src/auth/auth.dart b/lib/src/auth/auth.dart new file mode 100644 index 000000000..d128abe76 --- /dev/null +++ b/lib/src/auth/auth.dart @@ -0,0 +1,7 @@ +export 'auth_code_controller.dart'; +export 'auth_controller.dart'; +export 'authentication_server.dart'; +export 'authorization_parser.dart'; +export 'authorizer.dart'; +export 'client.dart'; +export 'protocols.dart'; \ No newline at end of file diff --git a/lib/auth/auth_code_controller.dart b/lib/src/auth/auth_code_controller.dart similarity index 97% rename from lib/auth/auth_code_controller.dart rename to lib/src/auth/auth_code_controller.dart index 4713ad9db..32047b29c 100644 --- a/lib/auth/auth_code_controller.dart +++ b/lib/src/auth/auth_code_controller.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import '../http/http.dart'; +import 'auth.dart'; /// [RequestController] for issuing OAuth 2.0 authorization codes. class AuthCodeController extends HTTPController { diff --git a/lib/auth/auth_controller.dart b/lib/src/auth/auth_controller.dart similarity index 98% rename from lib/auth/auth_controller.dart rename to lib/src/auth/auth_controller.dart index 5c57e1d1c..abb54721a 100644 --- a/lib/auth/auth_controller.dart +++ b/lib/src/auth/auth_controller.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import '../http/http.dart'; +import 'auth.dart'; /// [RequestController] for issuing OAuth 2.0 authorization tokens. class AuthController extends HTTPController { diff --git a/lib/auth/authentication_server.dart b/lib/src/auth/authentication_server.dart similarity index 98% rename from lib/auth/authentication_server.dart rename to lib/src/auth/authentication_server.dart index 4fc992395..72ce7e725 100644 --- a/lib/auth/authentication_server.dart +++ b/lib/src/auth/authentication_server.dart @@ -1,4 +1,16 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:mirrors'; + +import 'package:crypto/crypto.dart'; + +import '../http/http.dart'; +import '../utilities/pbkdf2.dart'; +import '../utilities/token_generator.dart'; +import 'auth.dart'; + /// A storage-agnostic authorization 'server'. /// diff --git a/lib/auth/authorization_parser.dart b/lib/src/auth/authorization_parser.dart similarity index 97% rename from lib/auth/authorization_parser.dart rename to lib/src/auth/authorization_parser.dart index f36e38217..44f492b59 100644 --- a/lib/auth/authorization_parser.dart +++ b/lib/src/auth/authorization_parser.dart @@ -1,4 +1,6 @@ -part of aqueduct; +import 'dart:convert'; + +import '../http/http.dart'; /// Parses a Bearer token from an Authorization header. class AuthorizationBearerParser { diff --git a/lib/auth/authorizer.dart b/lib/src/auth/authorizer.dart similarity index 97% rename from lib/auth/authorizer.dart rename to lib/src/auth/authorizer.dart index 0e0ebf9ee..7c894a41b 100644 --- a/lib/auth/authorizer.dart +++ b/lib/src/auth/authorizer.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import '../http/http.dart'; +import 'auth.dart'; /// The type of authentication strategy to use for an [Authorizer]. enum AuthStrategy { diff --git a/lib/auth/client.dart b/lib/src/auth/client.dart similarity index 97% rename from lib/auth/client.dart rename to lib/src/auth/client.dart index 2d7622d07..64aff14bd 100644 --- a/lib/auth/client.dart +++ b/lib/src/auth/client.dart @@ -1,4 +1,4 @@ -part of aqueduct; + /// Represents a Client ID and secret pair. class AuthClient { diff --git a/lib/auth/protocols.dart b/lib/src/auth/protocols.dart similarity index 98% rename from lib/auth/protocols.dart rename to lib/src/auth/protocols.dart index a30234dfc..1d48d0fe8 100644 --- a/lib/auth/protocols.dart +++ b/lib/src/auth/protocols.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'dart:async'; +import 'package:meta/meta.dart'; +import 'auth.dart'; +import '../db/managed/managed.dart'; /// An interface to represent [AuthServer.TokenType]. /// diff --git a/lib/commands/cli_command.dart b/lib/src/commands/cli_command.dart similarity index 86% rename from lib/commands/cli_command.dart rename to lib/src/commands/cli_command.dart index 9c90d5b4c..f1e1bdb7f 100644 --- a/lib/commands/cli_command.dart +++ b/lib/src/commands/cli_command.dart @@ -1,4 +1,10 @@ -part of aqueduct; +import 'dart:async'; + +import 'package:args/args.dart'; + +export 'migration_runner.dart'; +export 'setup_command.dart'; +export 'template_creator.dart'; /// A command line interface command. abstract class CLICommand { diff --git a/lib/commands/migration_runner.dart b/lib/src/commands/migration_runner.dart similarity index 95% rename from lib/commands/migration_runner.dart rename to lib/src/commands/migration_runner.dart index 98b6e5560..5d8743e5b 100644 --- a/lib/commands/migration_runner.dart +++ b/lib/src/commands/migration_runner.dart @@ -1,4 +1,12 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import 'package:yaml/yaml.dart'; +import 'package:safe_config/safe_config.dart'; +import 'package:args/args.dart'; + +import 'cli_command.dart'; +import '../db/db.dart'; /// Used internally. class CLIMigrationRunner extends CLICommand { @@ -133,7 +141,7 @@ class CLIMigrationRunner extends CLICommand { Future listVersions() async { var files = executor.migrationFiles.map((f) { var versionString = - "${executor._versionNumberFromFile(f)}".padLeft(8, "0"); + "${executor.versionNumberFromFile(f)}".padLeft(8, "0"); return " $versionString | ${f.path}"; }).join("\n"); @@ -169,7 +177,7 @@ class CLIMigrationRunner extends CLICommand { } Map versionMap = executor.migrationFiles.fold({}, (map, file) { - var versionNumber = executor._versionNumberFromFile(file); + var versionNumber = executor.versionNumberFromFile(file); map[versionNumber] = file; return map; }); diff --git a/lib/commands/setup_command.dart b/lib/src/commands/setup_command.dart similarity index 97% rename from lib/commands/setup_command.dart rename to lib/src/commands/setup_command.dart index 1889a6b2d..3356dfa81 100644 --- a/lib/commands/setup_command.dart +++ b/lib/src/commands/setup_command.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import 'package:args/args.dart'; + +import 'cli_command.dart'; class CLISetup extends CLICommand { ArgParser options = new ArgParser(allowTrailingOptions: false) diff --git a/lib/commands/template_creator.dart b/lib/src/commands/template_creator.dart similarity index 98% rename from lib/commands/template_creator.dart rename to lib/src/commands/template_creator.dart index a5d82c70a..49fc76892 100644 --- a/lib/commands/template_creator.dart +++ b/lib/src/commands/template_creator.dart @@ -1,4 +1,11 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as path_lib; + +import '../http/documentable.dart'; +import 'cli_command.dart'; /// Used internally. class CLITemplateCreator extends CLICommand { diff --git a/lib/src/db/db.dart b/lib/src/db/db.dart new file mode 100644 index 000000000..55b52f791 --- /dev/null +++ b/lib/src/db/db.dart @@ -0,0 +1,5 @@ +export 'managed/managed.dart'; +export 'persistent_store/persistent_store.dart'; +export 'postgresql/postgresql_persistent_store.dart'; +export 'query/query.dart'; +export 'schema/schema.dart'; \ No newline at end of file diff --git a/lib/db/managed/attributes.dart b/lib/src/db/managed/attributes.dart similarity index 99% rename from lib/db/managed/attributes.dart rename to lib/src/db/managed/attributes.dart index 7526c4378..36713156a 100644 --- a/lib/db/managed/attributes.dart +++ b/lib/src/db/managed/attributes.dart @@ -1,4 +1,4 @@ -part of aqueduct; +import 'managed.dart'; /// Possible values for a delete rule in a [ManagedRelationship]. enum ManagedRelationshipDeleteRule { diff --git a/lib/db/managed/backing.dart b/lib/src/db/managed/backing.dart similarity index 83% rename from lib/db/managed/backing.dart rename to lib/src/db/managed/backing.dart index 192e22a98..d6a1c8bcf 100644 --- a/lib/db/managed/backing.dart +++ b/lib/src/db/managed/backing.dart @@ -1,17 +1,9 @@ -part of aqueduct; +import 'dart:mirrors'; +import '../query/query.dart'; +import 'managed.dart'; +import '../query/matcher_internal.dart'; -abstract class _ManagedBacking { - dynamic valueForProperty(ManagedEntity entity, String propertyName); - void setValueForProperty( - ManagedEntity entity, String propertyName, dynamic value); - void removeProperty(String propertyName) { - valueMap.remove(propertyName); - } - - Map get valueMap; -} - -class _ManagedValueBacking extends _ManagedBacking { +class ManagedValueBacking extends ManagedBacking { Map valueMap = {}; dynamic valueForProperty(ManagedEntity entity, String propertyName) { @@ -44,7 +36,7 @@ class _ManagedValueBacking extends _ManagedBacking { } } -class _ManagedMatcherBacking extends _ManagedBacking { +class ManagedMatcherBacking extends ManagedBacking { Map valueMap = {}; dynamic valueForProperty(ManagedEntity entity, String propertyName) { @@ -55,7 +47,7 @@ class _ManagedMatcherBacking extends _ManagedBacking { ..entity = relDesc.destinationEntity; } else if (relDesc?.relationshipType == ManagedRelationshipType.hasOne) { valueMap[propertyName] = relDesc.destinationEntity.newInstance() - .._backing = new _ManagedMatcherBacking(); + ..backing = new ManagedMatcherBacking(); } else if (relDesc?.relationshipType == ManagedRelationshipType.belongsTo) { throw new QueryException(QueryExceptionEvent.requestFailure, @@ -74,7 +66,7 @@ class _ManagedMatcherBacking extends _ManagedBacking { return; } - if (value is _MatcherExpression) { + if (value is MatcherExpression) { var relDesc = entity.relationships[propertyName]; if (relDesc != null && @@ -94,7 +86,7 @@ class _ManagedMatcherBacking extends _ManagedBacking { } valueMap[propertyName] = - new _ComparisonMatcherExpression(value, MatcherOperator.equalTo); + new ComparisonMatcherExpression(value, MatcherOperator.equalTo); } } } diff --git a/lib/db/managed/context.dart b/lib/src/db/managed/context.dart similarity index 90% rename from lib/db/managed/context.dart rename to lib/src/db/managed/context.dart index f716ffb92..9ea99f46c 100644 --- a/lib/db/managed/context.dart +++ b/lib/src/db/managed/context.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:async'; + +import '../persistent_store/persistent_store_query.dart'; +import 'managed.dart'; +import '../persistent_store/persistent_store.dart'; +import '../query/query.dart'; /// Coordinates with a [ManagedDataModel] and [PersistentStore] to execute queries and /// translate between [ManagedObject] objects and database rows. @@ -42,22 +47,22 @@ class ManagedContext { return dataModel.entityForType(type); } - Future _executeInsertQuery(Query query) async { - var psq = new PersistentStoreQuery(query.entity, persistentStore, query); + Future executeInsertQuery(Query query) async { + var psq = query.persistentQueryForStore(persistentStore); var results = await persistentStore.executeInsertQuery(psq); return query.entity.instanceFromMappingElements(results); } - Future> _executeFetchQuery(Query query) async { - var psq = new PersistentStoreQuery(query.entity, persistentStore, query); + Future> executeFetchQuery(Query query) async { + var psq = query.persistentQueryForStore(persistentStore); var results = await persistentStore.executeFetchQuery(psq); return _coalesceAndMapRows(results, query.entity); } - Future> _executeUpdateQuery(Query query) async { - var psq = new PersistentStoreQuery(query.entity, persistentStore, query); + Future> executeUpdateQuery(Query query) async { + var psq = query.persistentQueryForStore(persistentStore); var results = await persistentStore.executeUpdateQuery(psq); return results.map((row) { @@ -65,9 +70,9 @@ class ManagedContext { }).toList(); } - Future _executeDeleteQuery(Query query) async { + Future executeDeleteQuery(Query query) async { return await persistentStore.executeDeleteQuery( - new PersistentStoreQuery(query.entity, persistentStore, query)); + query.persistentQueryForStore(persistentStore)); } List _coalesceAndMapRows( diff --git a/lib/db/managed/data_model.dart b/lib/src/db/managed/data_model.dart similarity index 90% rename from lib/db/managed/data_model.dart rename to lib/src/db/managed/data_model.dart index 10bc183f0..df7fc71bb 100644 --- a/lib/db/managed/data_model.dart +++ b/lib/src/db/managed/data_model.dart @@ -1,4 +1,6 @@ -part of aqueduct; +import 'dart:mirrors'; +import 'managed.dart'; +import 'data_model_builder.dart'; /// Instances of this class contain descriptions and metadata for mapping [ManagedObject]s to database rows. /// @@ -15,7 +17,7 @@ class ManagedDataModel { /// /// new DataModel([User, Token, Post]); ManagedDataModel(List instanceTypes) { - var builder = new _DataModelBuilder(this, instanceTypes); + var builder = new DataModelBuilder(this, instanceTypes); _entities = builder.entities; _persistentTypeToEntityMap = builder.persistentTypeToEntityMap; } @@ -28,7 +30,7 @@ class ManagedDataModel { LibraryMirror libMirror = reflectType(type).owner; var builder = - new _DataModelBuilder(this, _modelTypesFromLibraryMirror(libMirror)); + new DataModelBuilder(this, _modelTypesFromLibraryMirror(libMirror)); _entities = builder.entities; _persistentTypeToEntityMap = builder.persistentTypeToEntityMap; } @@ -42,10 +44,11 @@ class ManagedDataModel { } var libMirror = currentMirrorSystem().libraries[libraryURI]; var builder = - new _DataModelBuilder(this, _modelTypesFromLibraryMirror(libMirror)); + new DataModelBuilder(this, _modelTypesFromLibraryMirror(libMirror)); _entities = builder.entities; } + Iterable get entities => _entities.values; Map _entities = {}; Map _persistentTypeToEntityMap = {}; diff --git a/lib/db/managed/data_model_builder.dart b/lib/src/db/managed/data_model_builder.dart similarity index 92% rename from lib/db/managed/data_model_builder.dart rename to lib/src/db/managed/data_model_builder.dart index aeff36f59..1f5f41b60 100644 --- a/lib/db/managed/data_model_builder.dart +++ b/lib/src/db/managed/data_model_builder.dart @@ -1,25 +1,18 @@ -part of aqueduct; +import 'dart:mirrors'; +import 'managed.dart'; +import '../../utilities/mirror_helpers.dart'; -class _DataModelBuilder { - _DataModelBuilder(ManagedDataModel dataModel, List instanceTypes) { +class DataModelBuilder { + DataModelBuilder(ManagedDataModel dataModel, List instanceTypes) { instanceTypes.forEach((type) { + var backingMirror = backingMirrorForType(type); var entity = new ManagedEntity( - dataModel, reflectClass(type), backingMirrorForType(type)); + dataModel, tableNameForPersistentTypeMirror(backingMirror), + reflectClass(type), backingMirror); entities[type] = entity; persistentTypeToEntityMap[entity.persistentType.reflectedType] = entity; - }); - entities.forEach((_, entity) { - entity._tableName = tableNameForEntity(entity); entity.attributes = attributeMapForEntity(entity); - entity._primaryKey = entity.attributes.values - .firstWhere((attrDesc) => attrDesc.isPrimaryKey, orElse: () => null) - ?.name; - - if (entity.primaryKey == null) { - throw new ManagedDataModelException( - "No primary key for entity ${MirrorSystem.getName(entity.persistentType.simpleName)}"); - } }); entities.forEach((_, entity) { @@ -30,13 +23,13 @@ class _DataModelBuilder { Map entities = {}; Map persistentTypeToEntityMap = {}; - String tableNameForEntity(ManagedEntity entity) { + String tableNameForPersistentTypeMirror(ClassMirror typeMirror) { var tableNameSymbol = #tableName; - if (entity.persistentType.staticMembers[tableNameSymbol] != null) { - return entity.persistentType.invoke(tableNameSymbol, []).reflectee; + if (typeMirror.staticMembers[tableNameSymbol] != null) { + return typeMirror.invoke(tableNameSymbol, []).reflectee; } - return MirrorSystem.getName(entity.persistentType.simpleName); + return MirrorSystem.getName(typeMirror.simpleName); } Map attributeMapForEntity( diff --git a/lib/db/managed/entity.dart b/lib/src/db/managed/entity.dart similarity index 93% rename from lib/db/managed/entity.dart rename to lib/src/db/managed/entity.dart index 8de3c564e..ea3c105bb 100644 --- a/lib/db/managed/entity.dart +++ b/lib/src/db/managed/entity.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:mirrors'; +import 'managed.dart'; +import '../../http/documentable.dart'; +import '../query/query.dart'; +import '../persistent_store/persistent_store_query.dart'; /// Mapping information between a table in a database and a [ManagedObject] object. /// @@ -22,7 +26,7 @@ class ManagedEntity { /// Creates an instance of this type.. /// /// You should never call this method directly, it will be called by [ManagedDataModel]. - ManagedEntity(this.dataModel, this.instanceType, this.persistentType); + ManagedEntity(this.dataModel, this._tableName, this.instanceType, this.persistentType); /// The type of instances represented by this entity. /// @@ -61,7 +65,20 @@ class ManagedEntity { /// transient property declared in the instance type. /// The keys are the case-sensitive name of the attribute. Values that represent a relationship to another object /// are not stored in [attributes]. - Map attributes; + + Map _attributes; + Map get attributes => _attributes; + void set attributes(Map m) { + _attributes = m; + _primaryKey = m.values + .firstWhere((attrDesc) => attrDesc.isPrimaryKey, orElse: () => null) + ?.name; + + if (_primaryKey == null) { + throw new ManagedDataModelException( + "No primary key for entity ${MirrorSystem.getName(persistentType.simpleName)}"); + } + } /// All relationship values of this entity. /// diff --git a/lib/src/db/managed/managed.dart b/lib/src/db/managed/managed.dart new file mode 100644 index 000000000..bd1b3b628 --- /dev/null +++ b/lib/src/db/managed/managed.dart @@ -0,0 +1,8 @@ +export 'attributes.dart'; +export 'context.dart'; +export 'data_model.dart'; +export 'data_model_builder.dart'; +export 'entity.dart'; +export 'object.dart'; +export 'property_description.dart'; +export 'set.dart'; diff --git a/lib/db/managed/object.dart b/lib/src/db/managed/object.dart similarity index 90% rename from lib/db/managed/object.dart rename to lib/src/db/managed/object.dart index a4df55c20..b360c735d 100644 --- a/lib/db/managed/object.dart +++ b/lib/src/db/managed/object.dart @@ -1,4 +1,20 @@ -part of aqueduct; +import 'dart:mirrors'; + +import '../../http/serializable.dart'; +import 'managed.dart'; +import 'query_matchable.dart'; +import 'backing.dart'; + +abstract class ManagedBacking { + dynamic valueForProperty(ManagedEntity entity, String propertyName); + void setValueForProperty( + ManagedEntity entity, String propertyName, dynamic value); + void removeProperty(String propertyName) { + valueMap.remove(propertyName); + } + + Map get valueMap; +} /// An object whose storage is managed by an underlying [Map]. /// @@ -25,7 +41,7 @@ part of aqueduct; /// @primaryKey int id; // Persisted /// } class ManagedObject extends Object - with _QueryMatchableExtension + with QueryMatchableExtension implements HTTPSerializable, QueryMatchable { /// Used when building a [Query] to include instances of this type. /// @@ -49,25 +65,24 @@ class ManagedObject extends Object /// Not all values are fetched or populated in a [ManagedObject] instance. This value contains /// key-value pairs for the managed object that have been set, either manually /// or when fetched from a database. When [ManagedObject] is instantiated, this map is empty. - Map get backingMap => _backing.valueMap; + Map get backingMap => backing.valueMap; - _ManagedBacking _backing = new _ManagedValueBacking(); - Map get _matcherMap => backingMap; + ManagedBacking backing = new ManagedValueBacking(); /// Retrieves a value by property name from the [backingMap]. dynamic operator [](String propertyName) => - _backing.valueForProperty(entity, propertyName); + backing.valueForProperty(entity, propertyName); /// Sets a value by property name in the [backingMap]. void operator []=(String propertyName, dynamic value) { - _backing.setValueForProperty(entity, propertyName, value); + backing.setValueForProperty(entity, propertyName, value); } /// Removes a property from the [backingMap]. /// /// This will remove a value from the backing map. void removePropertyFromBackingMap(String propertyName) { - _backing.removeProperty(propertyName); + backing.removeProperty(propertyName); } /// Checks whether or not a property has been set in this instances' [backingMap]. @@ -114,7 +129,7 @@ class ManagedObject extends Object if (property is ManagedAttributeDescription) { if (!property.isTransient) { - _backing.setValueForProperty(entity, k, _valueDecoder(property, v)); + backing.setValueForProperty(entity, k, _valueDecoder(property, v)); } else { if (!property.transientStatus.isAvailableAsInput) { throw new QueryException(QueryExceptionEvent.requestFailure, @@ -134,7 +149,7 @@ class ManagedObject extends Object mirror.setField(new Symbol(k), decodedValue); } } else { - _backing.setValueForProperty(entity, k, _valueDecoder(property, v)); + backing.setValueForProperty(entity, k, _valueDecoder(property, v)); } }); } @@ -151,7 +166,7 @@ class ManagedObject extends Object Map asMap() { var outputMap = {}; - _backing.valueMap.forEach((k, v) { + backing.valueMap.forEach((k, v) { outputMap[k] = _valueEncoder(k, v); }); diff --git a/lib/db/managed/property_description.dart b/lib/src/db/managed/property_description.dart similarity index 99% rename from lib/db/managed/property_description.dart rename to lib/src/db/managed/property_description.dart index ccf3f90ac..40eae4117 100644 --- a/lib/db/managed/property_description.dart +++ b/lib/src/db/managed/property_description.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'dart:mirrors'; +import 'managed.dart'; /// Possible data types for [ManagedEntity] attributes. enum ManagedPropertyType { diff --git a/lib/src/db/managed/query_matchable.dart b/lib/src/db/managed/query_matchable.dart new file mode 100644 index 000000000..1948fc658 --- /dev/null +++ b/lib/src/db/managed/query_matchable.dart @@ -0,0 +1,24 @@ +import 'attributes.dart'; +import '../query/query.dart'; + +export '../query/query.dart'; + +abstract class QueryMatchableExtension implements QueryMatchable { + bool get hasJoinElements { + return backingMap.values + .where((item) => item is QueryMatchable) + .any((QueryMatchable item) => item.includeInResultSet); + } + + List get joinPropertyKeys { + return backingMap.keys.where((propertyName) { + var val = backingMap[propertyName]; + var relDesc = entity.relationships[propertyName]; + + return val is QueryMatchable && + val.includeInResultSet && + (relDesc?.relationshipType == ManagedRelationshipType.hasMany || + relDesc?.relationshipType == ManagedRelationshipType.hasOne); + }).toList(); + } +} diff --git a/lib/db/managed/set.dart b/lib/src/db/managed/set.dart similarity index 91% rename from lib/db/managed/set.dart rename to lib/src/db/managed/set.dart index 279908d0d..0869106f3 100644 --- a/lib/db/managed/set.dart +++ b/lib/src/db/managed/set.dart @@ -1,4 +1,10 @@ -part of aqueduct; +import 'dart:collection'; + +import 'backing.dart'; +import '../../http/serializable.dart'; +import 'managed.dart'; +import 'query_matchable.dart'; + /// Instances of this type contain zero or more instances of [ManagedObject]. /// @@ -18,7 +24,7 @@ part of aqueduct; /// User user; /// } class ManagedSet extends Object - with ListMixin, _QueryMatchableExtension + with ListMixin, QueryMatchableExtension implements QueryMatchable, HTTPSerializable { /// Creates an empty [ManagedSet]. ManagedSet() { @@ -58,7 +64,7 @@ class ManagedSet extends Object InstanceType get matchOn { if (_matchOn == null) { _matchOn = entity.newInstance() as InstanceType; - _matchOn._backing = new _ManagedMatcherBacking(); + _matchOn.backing = new ManagedMatcherBacking(); } return _matchOn; } @@ -69,7 +75,7 @@ class ManagedSet extends Object _innerValues.length = newLength; } - Map get _matcherMap => matchOn.backingMap; + Map get backingMap => matchOn.backingMap; List _innerValues; InstanceType _matchOn; diff --git a/lib/db/persistent_store/persistent_store.dart b/lib/src/db/persistent_store/persistent_store.dart similarity index 94% rename from lib/db/persistent_store/persistent_store.dart rename to lib/src/db/persistent_store/persistent_store.dart index e33e53ef8..6b95d0b00 100644 --- a/lib/db/persistent_store/persistent_store.dart +++ b/lib/src/db/persistent_store/persistent_store.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:async'; +import '../query/query.dart'; +import '../schema/schema.dart'; +import 'persistent_store_query.dart'; +import '../managed/managed.dart'; +export 'persistent_store_query.dart'; /// An interface for implementing persistent storage. /// diff --git a/lib/src/db/persistent_store/persistent_store_query.dart b/lib/src/db/persistent_store/persistent_store_query.dart new file mode 100644 index 000000000..8247419d6 --- /dev/null +++ b/lib/src/db/persistent_store/persistent_store_query.dart @@ -0,0 +1,71 @@ +import 'persistent_store.dart'; +import '../query/query.dart'; +import '../managed/managed.dart'; +import '../managed/query_matchable.dart'; + +/// This enumeration is used internaly. +enum PersistentJoinType { leftOuter } + +/// This class is used internally to map [Query] to something a [PersistentStore] can execute. +class PersistentStoreQuery { + int offset = 0; + int fetchLimit = 0; + int timeoutInSeconds = 30; + bool confirmQueryModifiesAllInstancesOnDeleteOrUpdate; + ManagedEntity entity; + QueryPage pageDescriptor; + QueryPredicate predicate; + List sortDescriptors; + List values; + List resultKeys; +} + +/// This class is used internally. +class PersistentColumnMapping { + PersistentColumnMapping(this.property, this.value); + PersistentColumnMapping.fromElement( + PersistentColumnMapping original, this.value) { + property = original.property; + } + + ManagedPropertyDescription property; + dynamic value; + + String toString() { + return "MappingElement on $property (Value = $value)"; + } +} + +/// This class is used internally. +class PersistentJoinMapping extends PersistentColumnMapping { + PersistentJoinMapping(this.type, ManagedPropertyDescription property, + this.predicate, this.resultKeys) + : super(property, null) { + var primaryKeyElement = this.resultKeys.firstWhere((e) { + var eProp = e.property; + if (eProp is ManagedAttributeDescription) { + return eProp.isPrimaryKey; + } + return false; + }); + + primaryKeyIndex = this.resultKeys.indexOf(primaryKeyElement); + } + + PersistentJoinMapping.fromElement( + PersistentJoinMapping original, List values) + : super.fromElement(original, values) { + type = original.type; + primaryKeyIndex = original.primaryKeyIndex; + } + + PersistentJoinType type; + ManagedPropertyDescription get joinProperty => + (property as ManagedRelationshipDescription).inverseRelationship; + QueryPredicate predicate; + List resultKeys; + + int primaryKeyIndex; + List get values => + value as List; +} diff --git a/lib/db/postgresql/postgresql_persistent_store.dart b/lib/src/db/postgresql/postgresql_persistent_store.dart similarity index 94% rename from lib/db/postgresql/postgresql_persistent_store.dart rename to lib/src/db/postgresql/postgresql_persistent_store.dart index 73a94657f..3f9988c69 100644 --- a/lib/db/postgresql/postgresql_persistent_store.dart +++ b/lib/src/db/postgresql/postgresql_persistent_store.dart @@ -1,4 +1,13 @@ -part of aqueduct; +import 'package:logging/logging.dart'; +import 'package:postgres/postgres.dart'; +import 'dart:async'; +import '../managed/managed.dart'; +import '../query/query.dart'; +import '../persistent_store/persistent_store.dart'; +import '../schema/schema.dart'; +import '../persistent_store/persistent_store_query.dart'; + +import 'postgresql_schema_generator.dart'; /// A function that will create an opened instance of [PostgreSQLConnection] when executed. typedef Future PostgreSQLConnectionFunction(); @@ -8,7 +17,7 @@ typedef Future PostgreSQLConnectionFunction(); /// To interact with a PostgreSQL database, a [ManagedContext] must have an instance of this class. /// Instances of this class are configured to connect to a particular PostgreSQL database. class PostgreSQLPersistentStore extends PersistentStore - with _PostgreSQLSchemaGenerator { + with PostgreSQLSchemaGenerator { /// The logger used by instances of this class. static Logger logger = new Logger("aqueduct"); @@ -138,7 +147,7 @@ class PostgreSQLPersistentStore extends PersistentStore Future get schemaVersion async { try { var values = await execute( - "SELECT versionNumber, dateOfUpgrade FROM $_versionTableName ORDER BY dateOfUpgrade ASC") + "SELECT versionNumber, dateOfUpgrade FROM $versionTableName ORDER BY dateOfUpgrade ASC") as List>; if (values.length == 0) { return 0; @@ -164,7 +173,7 @@ class PostgreSQLPersistentStore extends PersistentStore try { await connection.transaction((ctx) async { var existingVersionRows = await ctx.query( - "SELECT versionNumber, dateOfUpgrade FROM $_versionTableName WHERE versionNumber=@v:int4", + "SELECT versionNumber, dateOfUpgrade FROM $versionTableName WHERE versionNumber=@v:int4", substitutionValues: {"v": versionNumber}); if (existingVersionRows.length > 0) { var date = existingVersionRows.first.last; @@ -177,7 +186,7 @@ class PostgreSQLPersistentStore extends PersistentStore } await ctx.execute( - "INSERT INTO $_versionTableName (versionNumber, dateOfUpgrade) VALUES ($versionNumber, '${new DateTime.now().toUtc().toIso8601String()}')"); + "INSERT INTO $versionTableName (versionNumber, dateOfUpgrade) VALUES ($versionNumber, '${new DateTime.now().toUtc().toIso8601String()}')"); }); } on PostgreSQLException catch (e) { throw _interpretException(e); @@ -201,7 +210,7 @@ class PostgreSQLPersistentStore extends PersistentStore var queryStringBuffer = new StringBuffer(); queryStringBuffer.write( - "INSERT INTO ${q.rootEntity.tableName} ($columnsBeingInserted) "); + "INSERT INTO ${q.entity.tableName} ($columnsBeingInserted) "); queryStringBuffer.write("VALUES (${valueKeysToBeInserted}) "); if (q.resultKeys != null && q.resultKeys.length > 0) { @@ -237,7 +246,7 @@ class PostgreSQLPersistentStore extends PersistentStore }).join(","); var queryStringBuffer = new StringBuffer( - "SELECT $columnsToFetch FROM ${q.rootEntity.tableName} "); + "SELECT $columnsToFetch FROM ${q.entity.tableName} "); joinElements.forEach((PersistentColumnMapping je) { PersistentJoinMapping joinElement = je; queryStringBuffer.write("${_joinStringForJoin(joinElement)} "); @@ -282,7 +291,7 @@ class PostgreSQLPersistentStore extends PersistentStore Map valueMap = null; var queryStringBuffer = new StringBuffer(); - queryStringBuffer.write("DELETE FROM ${q.rootEntity.tableName} "); + queryStringBuffer.write("DELETE FROM ${q.entity.tableName} "); if (q.predicate != null) { queryStringBuffer.write("where ${q.predicate.format} "); @@ -320,7 +329,7 @@ class PostgreSQLPersistentStore extends PersistentStore var queryStringBuffer = new StringBuffer(); queryStringBuffer - .write("UPDATE ${q.rootEntity.tableName} SET $setPairString "); + .write("UPDATE ${q.entity.tableName} SET $setPairString "); if (q.predicate != null) { queryStringBuffer.write("where ${q.predicate.format} "); @@ -536,7 +545,7 @@ class PostgreSQLPersistentStore extends PersistentStore } var joinedSortDescriptors = sortDescs.map((QuerySortDescriptor sd) { - var property = q.rootEntity.properties[sd.key]; + var property = q.entity.properties[sd.key]; var columnName = "${property.entity.tableName}.${_columnNameForProperty(property)}"; var order = (sd.order == QuerySortOrder.ascending ? "ASC" : "DESC"); @@ -555,7 +564,7 @@ class PostgreSQLPersistentStore extends PersistentStore (query.pageDescriptor.order == QuerySortOrder.ascending ? ">" : "<"); var keyName = "aq_page_value"; var typedKeyName = _typedColumnName(keyName, - query.rootEntity.properties[query.pageDescriptor.propertyName]); + query.entity.properties[query.pageDescriptor.propertyName]); return new QueryPredicate( "${query.pageDescriptor.propertyName} ${operator} @$typedKeyName", {"$keyName": query.pageDescriptor.boundingValue}); @@ -584,7 +593,7 @@ class PostgreSQLPersistentStore extends PersistentStore Future _createVersionTableIfNecessary(bool temporary) async { var conn = await getDatabaseConnection(); - var commands = createTable(_versionTable, isTemporary: temporary); + var commands = createTable(versionTable, isTemporary: temporary); try { await conn.transaction((ctx) async { for (var cmd in commands) { diff --git a/lib/db/postgresql/postgresql_schema_generator.dart b/lib/src/db/postgresql/postgresql_schema_generator.dart similarity index 93% rename from lib/db/postgresql/postgresql_schema_generator.dart rename to lib/src/db/postgresql/postgresql_schema_generator.dart index 4bac7ba4c..332ae9511 100644 --- a/lib/db/postgresql/postgresql_schema_generator.dart +++ b/lib/src/db/postgresql/postgresql_schema_generator.dart @@ -1,7 +1,8 @@ -part of aqueduct; +import '../schema/schema.dart'; +import '../managed/managed.dart'; -class _PostgreSQLSchemaGenerator { - String get _versionTableName => "_aqueduct_version_pgsql"; +class PostgreSQLSchemaGenerator { + String get versionTableName => "_aqueduct_version_pgsql"; List createTable(SchemaTable table, {bool isTemporary: false}) { var columnString = @@ -142,7 +143,7 @@ class _PostgreSQLSchemaGenerator { return [ "ALTER TABLE ONLY ${tableName} ADD FOREIGN KEY (${_columnNameForColumn(column)}) " "REFERENCES ${column.relatedTableName} (${column.relatedColumnName}) " - "ON DELETE ${_deleteRuleStringForDeleteRule(column._deleteRule)}" + "ON DELETE ${_deleteRuleStringForDeleteRule(SchemaColumn.deleteRuleStringForDeleteRule(column.deleteRule))}" ]; } @@ -191,7 +192,7 @@ class _PostgreSQLSchemaGenerator { } String _postgreSQLTypeForColumn(SchemaColumn t) { - switch (t._type) { + switch (t.typeString) { case "integer": { if (t.autoincrement) { @@ -221,18 +222,16 @@ class _PostgreSQLSchemaGenerator { return null; } - SchemaTable get _versionTable { + SchemaTable get versionTable { return new SchemaTable.empty() - ..name = _versionTableName + ..name = versionTableName ..columns = [ (new SchemaColumn.empty() ..name = "versionNumber" - .._type = - SchemaColumn.typeStringForType(ManagedPropertyType.integer)), + ..type = ManagedPropertyType.integer), (new SchemaColumn.empty() ..name = "dateOfUpgrade" - .._type = - SchemaColumn.typeStringForType(ManagedPropertyType.datetime)), + ..type = ManagedPropertyType.datetime), ]; } } diff --git a/lib/db/query/matcher_expression.dart b/lib/src/db/query/matcher_expression.dart similarity index 72% rename from lib/db/query/matcher_expression.dart rename to lib/src/db/query/matcher_expression.dart index 5a54502da..df789e500 100644 --- a/lib/db/query/matcher_expression.dart +++ b/lib/src/db/query/matcher_expression.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'matcher_internal.dart'; +import 'query.dart'; /// The operator in a comparison matcher. enum MatcherOperator { @@ -20,7 +21,7 @@ enum StringMatcherOperator { beginsWith, contains, endsWith } /// var query = new Query() /// ..matchOn.id = whereEqualTo(1); dynamic whereEqualTo(dynamic value) { - return new _ComparisonMatcherExpression(value, MatcherOperator.equalTo); + return new ComparisonMatcherExpression(value, MatcherOperator.equalTo); } /// Matcher for matching a column value greater than the argument in a [Query]. @@ -30,7 +31,7 @@ dynamic whereEqualTo(dynamic value) { /// var query = new Query() /// ..matchOn.salary = whereGreaterThan(60000); dynamic whereGreaterThan(dynamic value) { - return new _ComparisonMatcherExpression(value, MatcherOperator.greaterThan); + return new ComparisonMatcherExpression(value, MatcherOperator.greaterThan); } /// Matcher for matching a column value greater than or equal to the argument in a [Query]. @@ -40,7 +41,7 @@ dynamic whereGreaterThan(dynamic value) { /// var query = new Query() /// ..matchOn.salary = whereGreaterThanEqualTo(60000); dynamic whereGreaterThanEqualTo(dynamic value) { - return new _ComparisonMatcherExpression( + return new ComparisonMatcherExpression( value, MatcherOperator.greaterThanEqualTo); } @@ -51,7 +52,7 @@ dynamic whereGreaterThanEqualTo(dynamic value) { /// var query = new Query() /// ..matchOn.salary = whereLessThan(60000); dynamic whereLessThan(dynamic value) { - return new _ComparisonMatcherExpression(value, MatcherOperator.lessThan); + return new ComparisonMatcherExpression(value, MatcherOperator.lessThan); } /// Matcher for matching a column value less than or equal to the argument in a [Query]. @@ -61,7 +62,7 @@ dynamic whereLessThan(dynamic value) { /// var query = new Query() /// ..matchOn.salary = whereLessThanEqualTo(60000); dynamic whereLessThanEqualTo(dynamic value) { - return new _ComparisonMatcherExpression( + return new ComparisonMatcherExpression( value, MatcherOperator.lessThanEqualTo); } @@ -72,7 +73,7 @@ dynamic whereLessThanEqualTo(dynamic value) { /// var query = new Query() /// ..matchOn.id = whereNotEqual(60000); dynamic whereNotEqual(dynamic value) { - return new _ComparisonMatcherExpression(value, MatcherOperator.notEqual); + return new ComparisonMatcherExpression(value, MatcherOperator.notEqual); } /// Matcher for matching string properties that contain [value] in a [Query]. @@ -82,7 +83,7 @@ dynamic whereNotEqual(dynamic value) { /// var query = new Query() /// ..matchOn.title = whereContains("Director"); dynamic whereContains(String value) { - return new _StringMatcherExpression(value, StringMatcherOperator.contains); + return new StringMatcherExpression(value, StringMatcherOperator.contains); } /// Matcher for matching string properties that start with [value] in a [Query]. @@ -92,7 +93,7 @@ dynamic whereContains(String value) { /// var query = new Query() /// ..matchOn.name = whereBeginsWith("B"); dynamic whereBeginsWith(String value) { - return new _StringMatcherExpression(value, StringMatcherOperator.beginsWith); + return new StringMatcherExpression(value, StringMatcherOperator.beginsWith); } /// Matcher for matching string properties that end with [value] in a [Query]. @@ -102,7 +103,7 @@ dynamic whereBeginsWith(String value) { /// var query = new Query() /// ..matchOn.name = whereEndsWith("son"); dynamic whereEndsWith(String value) { - return new _StringMatcherExpression(value, StringMatcherOperator.endsWith); + return new StringMatcherExpression(value, StringMatcherOperator.endsWith); } /// Matcher for matching values that are within the list of [values] in a [Query]. @@ -112,7 +113,7 @@ dynamic whereEndsWith(String value) { /// var query = new Query() /// ..matchOn.department = whereIn(["Engineering", "HR"]); dynamic whereIn(Iterable values) { - return new _WithinMatcherExpression(values.toList()); + return new WithinMatcherExpression(values.toList()); } /// Matcher for matching column values where [lhs] <= value <= [rhs] in a [Query]. @@ -122,7 +123,7 @@ dynamic whereIn(Iterable values) { /// var query = new Query() /// ..matchOn.salary = whereBetween(80000, 100000); dynamic whereBetween(dynamic lhs, dynamic rhs) { - return new _RangeMatcherExpression(lhs, rhs, true); + return new RangeMatcherExpression(lhs, rhs, true); } /// Matcher for matching column values where matched value is less than [lhs] or greater than [rhs] in a [Query]. @@ -132,7 +133,7 @@ dynamic whereBetween(dynamic lhs, dynamic rhs) { /// var query = new Query() /// ..matchOn.salary = whereOutsideOf(80000, 100000); dynamic whereOutsideOf(dynamic lhs, dynamic rhs) { - return new _RangeMatcherExpression(lhs, rhs, false); + return new RangeMatcherExpression(lhs, rhs, false); } /// Matcher for matching [ManagedRelationship] property in a [Query]. @@ -146,7 +147,7 @@ dynamic whereOutsideOf(dynamic lhs, dynamic rhs) { /// var q = new Query() /// ..matchOn.user = whereRelatedByValue(userPrimaryKey); dynamic whereRelatedByValue(dynamic foreignKeyValue) { - return new _ComparisonMatcherExpression( + return new ComparisonMatcherExpression( foreignKeyValue, MatcherOperator.equalTo); } @@ -156,7 +157,7 @@ dynamic whereRelatedByValue(dynamic foreignKeyValue) { /// /// var q = new Query() /// ..matchOn.manager = whereNull; -const dynamic whereNull = const _NullMatcherExpression(true); +const dynamic whereNull = const NullMatcherExpression(true); /// Matcher for matching everything but null in a [Query]. /// @@ -164,42 +165,7 @@ const dynamic whereNull = const _NullMatcherExpression(true); /// /// var q = new Query() /// ..matchOn.manager = whereNotNull; -const dynamic whereNotNull = const _NullMatcherExpression(false); - -abstract class _MatcherExpression {} - -class _ComparisonMatcherExpression implements _MatcherExpression { - const _ComparisonMatcherExpression(this.value, this.operator); - - final dynamic value; - final MatcherOperator operator; -} - -class _RangeMatcherExpression implements _MatcherExpression { - const _RangeMatcherExpression(this.lhs, this.rhs, this.within); - - final bool within; - final dynamic lhs, rhs; -} - -class _NullMatcherExpression implements _MatcherExpression { - const _NullMatcherExpression(this.shouldBeNull); - - final bool shouldBeNull; -} - -class _WithinMatcherExpression implements _MatcherExpression { - _WithinMatcherExpression(this.values); - - List values; -} - -class _StringMatcherExpression implements _MatcherExpression { - _StringMatcherExpression(this.value, this.operator); - - StringMatcherOperator operator; - String value; -} +const dynamic whereNotNull = const NullMatcherExpression(false); /// Thrown when a [Query] matcher is invalid. class PredicateMatcherException implements Exception { diff --git a/lib/src/db/query/matcher_internal.dart b/lib/src/db/query/matcher_internal.dart new file mode 100644 index 000000000..af07e7ed7 --- /dev/null +++ b/lib/src/db/query/matcher_internal.dart @@ -0,0 +1,37 @@ +import 'query.dart'; + +abstract class MatcherExpression {} + +class ComparisonMatcherExpression implements MatcherExpression { + const ComparisonMatcherExpression(this.value, this.operator); + + final dynamic value; + final MatcherOperator operator; +} + +class RangeMatcherExpression implements MatcherExpression { + const RangeMatcherExpression(this.lhs, this.rhs, this.within); + + final bool within; + final dynamic lhs, rhs; +} + +class NullMatcherExpression implements MatcherExpression { + const NullMatcherExpression(this.shouldBeNull); + + final bool shouldBeNull; +} + +class WithinMatcherExpression implements MatcherExpression { + WithinMatcherExpression(this.values); + + List values; +} + +class StringMatcherExpression implements MatcherExpression { + StringMatcherExpression(this.value, this.operator); + + StringMatcherOperator operator; + String value; +} + diff --git a/lib/db/query/page.dart b/lib/src/db/query/page.dart similarity index 99% rename from lib/db/query/page.dart rename to lib/src/db/query/page.dart index 0daf42f23..b217052d1 100644 --- a/lib/db/query/page.dart +++ b/lib/src/db/query/page.dart @@ -1,4 +1,4 @@ -part of aqueduct; +import 'query.dart'; /// A description of a page of results to be applied to a [Query]. /// diff --git a/lib/db/query/predicate.dart b/lib/src/db/query/predicate.dart similarity index 86% rename from lib/db/query/predicate.dart rename to lib/src/db/query/predicate.dart index 0a8fbc771..2ce570c10 100644 --- a/lib/db/query/predicate.dart +++ b/lib/src/db/query/predicate.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'query.dart'; +import '../managed/managed.dart'; +import '../persistent_store/persistent_store.dart'; +import '../query/matcher_internal.dart'; /// A predicate contains instructions for filtering rows when performing a [Query]. /// @@ -29,10 +32,10 @@ class QueryPredicate { /// The [format] and [parameters] of this predicate. QueryPredicate(this.format, this.parameters); - factory QueryPredicate._fromQueryIncludable( + factory QueryPredicate.fromQueryIncludable( QueryMatchable obj, PersistentStore persistentStore) { var entity = obj.entity; - var attributeKeys = obj._matcherMap.keys.where((propertyName) { + var attributeKeys = obj.backingMap.keys.where((propertyName) { var desc = entity.properties[propertyName]; if (desc is ManagedRelationshipDescription) { return desc.relationshipType == ManagedRelationshipType.belongsTo; @@ -43,19 +46,19 @@ class QueryPredicate { return QueryPredicate.andPredicates(attributeKeys.map((queryKey) { var desc = entity.properties[queryKey]; - var matcher = obj._matcherMap[queryKey]; + var matcher = obj.backingMap[queryKey]; - if (matcher is _ComparisonMatcherExpression) { + if (matcher is ComparisonMatcherExpression) { return persistentStore.comparisonPredicate( desc, matcher.operator, matcher.value); - } else if (matcher is _RangeMatcherExpression) { + } else if (matcher is RangeMatcherExpression) { return persistentStore.rangePredicate( desc, matcher.lhs, matcher.rhs, matcher.within); - } else if (matcher is _NullMatcherExpression) { + } else if (matcher is NullMatcherExpression) { return persistentStore.nullPredicate(desc, matcher.shouldBeNull); - } else if (matcher is _WithinMatcherExpression) { + } else if (matcher is WithinMatcherExpression) { return persistentStore.containsPredicate(desc, matcher.values); - } else if (matcher is _StringMatcherExpression) { + } else if (matcher is StringMatcherExpression) { return persistentStore.stringPredicate( desc, matcher.operator, matcher.value); } diff --git a/lib/db/query/query.dart b/lib/src/db/query/query.dart similarity index 84% rename from lib/db/query/query.dart rename to lib/src/db/query/query.dart index 0d9285efc..18a43d4ad 100644 --- a/lib/db/query/query.dart +++ b/lib/src/db/query/query.dart @@ -1,4 +1,18 @@ -part of aqueduct; +import 'dart:async'; + +import '../managed/managed.dart'; +import '../managed/backing.dart'; +import 'page.dart'; +import 'predicate.dart'; +import 'sort_descriptor.dart'; +import '../persistent_store/persistent_store.dart'; +import '../persistent_store/persistent_store_query.dart'; +import 'query_mapping.dart'; + +export 'matcher_expression.dart'; +export 'page.dart'; +export 'predicate.dart'; +export 'sort_descriptor.dart'; /// Contains information for building and executing a database operation. /// @@ -48,7 +62,7 @@ class Query { InstanceType get matchOn { if (_matchOn == null) { _matchOn = entity.newInstance() as InstanceType; - _matchOn._backing = new _ManagedMatcherBacking(); + _matchOn.backing = new ManagedMatcherBacking(); } return _matchOn; } @@ -125,6 +139,43 @@ class Query { InstanceType _valueObject; + PersistentStoreQuery persistentQueryForStore(PersistentStore store) { + var psq = new PersistentStoreQuery() + ..confirmQueryModifiesAllInstancesOnDeleteOrUpdate = confirmQueryModifiesAllInstancesOnDeleteOrUpdate + ..entity = entity + ..timeoutInSeconds = timeoutInSeconds + ..sortDescriptors = sortDescriptors + ..resultKeys = mappingElementsForList((resultProperties ?? entity.defaultProperties), entity); + + if (_matchOn != null) { + psq.predicate = new QueryPredicate.fromQueryIncludable(_matchOn, store); + } else { + psq.predicate = predicate; + } + + if (_matchOn?.hasJoinElements ?? false) { + if (pageDescriptor != null) { + throw new QueryException(QueryExceptionEvent.requestFailure, + message: + "Query cannot have properties that are includeInResultSet and also have a pageDescriptor."); + } + + var joinElements = joinElementsFromQueryMatchable( + matchOn, store, nestedResultProperties); + psq.resultKeys.addAll(joinElements); + } else { + psq.fetchLimit = fetchLimit; + psq.offset = offset; + + psq.pageDescriptor = validatePageDescriptor(entity, pageDescriptor); + + psq.values = mappingElementsForMap( + (valueMap ?? values?.backingMap), entity); + } + + return psq; + } + /// A list of properties to be fetched by this query. /// /// Each [InstanceType] will have these properties set when this query is executed. Each property must be @@ -151,7 +202,7 @@ class Query { /// ..values.name = "Joe"; /// var newUser = await q.insert(); Future insert() async { - return await context._executeInsertQuery(this); + return await context.executeInsertQuery(this); } /// Updates [InstanceType]s in the underlying database. @@ -167,7 +218,7 @@ class Query { /// ..values.name = "Joe"; /// var usersNamedFredNowNamedJoe = await q.update(); Future> update() async { - return await context._executeUpdateQuery(this); + return await context.executeUpdateQuery(this); } /// Updates an [InstanceType] in the underlying database. @@ -175,7 +226,7 @@ class Query { /// This method works the same as [update], except it may only update one row in the underlying database. If this method /// ends up modifying multiple rows, an exception is thrown. Future updateOne() async { - var results = await context._executeUpdateQuery(this); + var results = await context.executeUpdateQuery(this); if (results.length == 1) { return results.first; } else if (results.length == 0) { @@ -195,7 +246,7 @@ class Query { /// var allUsers = q.fetch(); /// Future> fetch() async { - return await context._executeFetchQuery(this); + return await context.executeFetchQuery(this); } /// Fetches a single [InstanceType] from the database. @@ -204,7 +255,7 @@ class Query { Future fetchOne() async { fetchLimit = 1; - var results = await context._executeFetchQuery(this); + var results = await context.executeFetchQuery(this); if (results.length == 1) { return results.first; } else if (results.length > 1) { @@ -228,7 +279,7 @@ class Query { /// ..matchOn.id = whereEqualTo(1); /// var deletedCount = await q.delete(); Future delete() async { - return await context._executeDeleteQuery(this); + return await context.executeDeleteQuery(this); } } @@ -275,31 +326,10 @@ enum QueryExceptionEvent { requestFailure } -/// Used internally. abstract class QueryMatchable { ManagedEntity entity; bool includeInResultSet; - Map get _matcherMap; -} - -abstract class _QueryMatchableExtension implements QueryMatchable { - bool get _hasJoinElements { - return _matcherMap.values - .where((item) => item is QueryMatchable) - .any((QueryMatchable item) => item.includeInResultSet); - } - - List get _joinPropertyKeys { - return _matcherMap.keys.where((propertyName) { - var val = _matcherMap[propertyName]; - var relDesc = entity.relationships[propertyName]; - - return val is QueryMatchable && - val.includeInResultSet && - (relDesc?.relationshipType == ManagedRelationshipType.hasMany || - relDesc?.relationshipType == ManagedRelationshipType.hasOne); - }).toList(); - } -} + Map get backingMap; +} \ No newline at end of file diff --git a/lib/src/db/query/query_mapping.dart b/lib/src/db/query/query_mapping.dart new file mode 100644 index 000000000..91bc40ec5 --- /dev/null +++ b/lib/src/db/query/query_mapping.dart @@ -0,0 +1,132 @@ +import 'dart:mirrors'; + +import 'query.dart'; +import '../managed/managed.dart'; +import '../persistent_store/persistent_store.dart'; +import '../managed/query_matchable.dart'; +import '../persistent_store/persistent_store_query.dart'; + +ManagedPropertyDescription _propertyForName(ManagedEntity entity, String propertyName) { + var property = entity.properties[propertyName]; + if (property == null) { + throw new QueryException(QueryExceptionEvent.internalFailure, + message: + "Property $propertyName does not exist on ${entity.tableName}"); + } + if (property is ManagedRelationshipDescription && + property.relationshipType != ManagedRelationshipType.belongsTo) { + throw new QueryException(QueryExceptionEvent.internalFailure, + message: + "Property $propertyName is a hasMany or hasOne relationship and is invalid as a result property of ${entity + .tableName}, use matchOn.$propertyName.includeInResultSet = true instead."); + } + + return property; +} + +List mappingElementsForList(List keys, ManagedEntity entity) { + if (!keys.contains(entity.primaryKey)) { + keys.add(entity.primaryKey); + } + + return keys.map((key) { + var property = _propertyForName(entity, key); + return new PersistentColumnMapping(property, null); + }).toList(); +} + +QueryPage validatePageDescriptor(ManagedEntity entity, QueryPage page) { + if (page == null) { + return null; + } + + var prop = entity.attributes[page.propertyName]; + if (prop == null) { + throw new QueryException(QueryExceptionEvent.requestFailure, + message: + "Property ${page.propertyName} in pageDescriptor does not exist on ${entity.tableName}."); + } + + if (page.boundingValue != null && + !prop.isAssignableWith(page.boundingValue)) { + throw new QueryException(QueryExceptionEvent.requestFailure, + message: + "Property ${page.propertyName} in pageDescriptor has invalid type (${page.boundingValue.runtimeType})."); + } + + return page; +} + +List mappingElementsForMap(Map valueMap, ManagedEntity entity) { + return valueMap?.keys + ?.map((key) { + var property = entity.properties[key]; + if (property == null) { + throw new QueryException(QueryExceptionEvent.requestFailure, + message: + "Property $key in values does not exist on ${entity.tableName}"); + } + + var value = valueMap[key]; + if (property is ManagedRelationshipDescription) { + if (property.relationshipType != + ManagedRelationshipType.belongsTo) { + return null; + } + + if (value != null) { + if (value is ManagedObject) { + value = value[property.destinationEntity.primaryKey]; + } else if (value is Map) { + value = value[property.destinationEntity.primaryKey]; + } else { + throw new QueryException(QueryExceptionEvent.internalFailure, + message: + "Property $key on ${entity.tableName} in Query values must be a Map or ${MirrorSystem.getName( + property.destinationEntity.instanceType.simpleName)} "); + } + } + } + + return new PersistentColumnMapping(property, value); + }) + ?.where((m) => m != null) + ?.toList(); +} + +List joinElementsFromQueryMatchable(QueryMatchableExtension matcherBackedObject, + PersistentStore store, + Map> nestedResultProperties) { + var entity = matcherBackedObject.entity; + var propertiesToJoin = matcherBackedObject.joinPropertyKeys; + + return propertiesToJoin + .map((propertyName) { + QueryMatchableExtension inner = + matcherBackedObject.backingMap[propertyName]; + + var relDesc = entity.relationships[propertyName]; + var predicate = new QueryPredicate.fromQueryIncludable(inner, store); + var nestedProperties = + nestedResultProperties[inner.entity.instanceType.reflectedType]; + var propertiesToFetch = + nestedProperties ?? inner.entity.defaultProperties; + + var joinElements = [ + new PersistentJoinMapping( + PersistentJoinType.leftOuter, + relDesc, + predicate, + mappingElementsForList(propertiesToFetch, inner.entity)) + ]; + + if (inner.hasJoinElements) { + joinElements.addAll(joinElementsFromQueryMatchable( + inner, store, nestedResultProperties)); + } + + return joinElements; + }) + .expand((l) => l) + .toList(); +} \ No newline at end of file diff --git a/lib/db/query/sort_descriptor.dart b/lib/src/db/query/sort_descriptor.dart similarity index 97% rename from lib/db/query/sort_descriptor.dart rename to lib/src/db/query/sort_descriptor.dart index fbb0a78af..e29fbdac6 100644 --- a/lib/db/query/sort_descriptor.dart +++ b/lib/src/db/query/sort_descriptor.dart @@ -1,4 +1,4 @@ -part of aqueduct; + /// Order value for [QuerySortDescriptor]s and [QueryPage]s. enum QuerySortOrder { diff --git a/lib/db/schema/migration.dart b/lib/src/db/schema/migration.dart similarity index 90% rename from lib/db/schema/migration.dart rename to lib/src/db/schema/migration.dart index 6fc5a3d4e..fcff0c3fe 100644 --- a/lib/db/schema/migration.dart +++ b/lib/src/db/schema/migration.dart @@ -1,4 +1,12 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:mirrors'; + +import '../managed/managed.dart'; +import '../persistent_store/persistent_store.dart'; +import 'schema.dart'; +import '../../utilities/source_generator.dart'; +import '../postgresql/postgresql_persistent_store.dart'; /// Thrown when [Migration] encounters an error. class MigrationException { @@ -77,7 +85,10 @@ class MigrationExecutor { return m; } catch (e) { throw new MigrationException( - "Migration files must have the following format: Version_Name.migration.dart, where Version must be an integer (optionally prefixed with 0s, e.g. '00000002') and '_Name' is optional. Offender: ${fse.uri}"); + "Migration files must have the following format: Version_Name.migration.dart," + "where Version must be an integer (optionally prefixed with 0s, e.g. '00000002')" + " and '_Name' is optional. Offender: ${fse.uri}" + ); } }); @@ -99,7 +110,7 @@ class MigrationExecutor { "Migration directory doesn't contain any migrations, nothing to validate."); } - var generator = new _SourceGenerator( + var generator = new SourceGenerator( (List args, Map values) async { var dataModel = new ManagedDataModel.fromURI( new Uri(scheme: "package", path: args[0])); @@ -114,7 +125,7 @@ class MigrationExecutor { "dart:async" ]); - var executor = new _IsolateExecutor(generator, [libraryName], + var executor = new IsolateExecutor(generator, [libraryName], packageConfigURI: projectDirectoryPath.resolve(".packages")); var projectSchema = new Schema.fromMap(await executor.execute( workingDirectory: projectDirectoryPath) as Map); @@ -142,7 +153,7 @@ class MigrationExecutor { var files = migrationFiles; if (!files.isEmpty) { // For now, just make a new empty one... - var newVersionNumber = _versionNumberFromFile(files.last) + 1; + var newVersionNumber = versionNumberFromFile(files.last) + 1; var contents = SchemaBuilder.sourceForSchemaUpgrade( new Schema.empty(), new Schema.empty(), newVersionNumber); var file = new File.fromUri(migrationFileDirectory.resolve( @@ -152,7 +163,7 @@ class MigrationExecutor { return file; } - var generator = new _SourceGenerator( + var generator = new SourceGenerator( (List args, Map values) async { var dataModel = new ManagedDataModel.fromURI( new Uri(scheme: "package", path: args[0])); @@ -168,7 +179,7 @@ class MigrationExecutor { "dart:async" ]); - var executor = new _IsolateExecutor(generator, [libraryName], + var executor = new IsolateExecutor(generator, [libraryName], packageConfigURI: projectDirectoryPath.resolve(".packages")); var contents = await executor.execute(workingDirectory: projectDirectoryPath); @@ -211,7 +222,7 @@ class MigrationExecutor { /////// - int _versionNumberFromFile(File file) { + int versionNumberFromFile(File file) { var fileName = file.uri.pathSegments.last; var migrationName = fileName.split(".").first; return int.parse(migrationName.split("_").first); @@ -221,7 +232,7 @@ class MigrationExecutor { var files = migrationFiles; var latestMigrationFile = files.last; var latestMigrationVersionNumber = - _versionNumberFromFile(latestMigrationFile); + versionNumberFromFile(latestMigrationFile); List migrationFilesToRun = []; List migrationFilesToGetToCurrent = []; @@ -229,7 +240,7 @@ class MigrationExecutor { migrationFilesToRun = files; } else if (latestMigrationVersionNumber > aroundVersion) { var indexOfCurrent = files.indexOf( - files.firstWhere((f) => _versionNumberFromFile(f) == aroundVersion)); + files.firstWhere((f) => versionNumberFromFile(f) == aroundVersion)); migrationFilesToGetToCurrent = files.sublist(0, indexOfCurrent + 1); migrationFilesToRun = files.sublist(indexOfCurrent + 1); } else { @@ -241,7 +252,7 @@ class MigrationExecutor { Future _executeUpgradeForFile(File file, Schema schema, {bool dryRun: false}) async { - var generator = new _SourceGenerator( + var generator = new SourceGenerator( (List args, Map values) async { var inputSchema = new Schema.fromMap(values["schema"] as Map); @@ -289,8 +300,8 @@ class MigrationExecutor { "dart:mirrors" ], additionalContents: file.readAsStringSync()); - var executor = new _IsolateExecutor(generator, [ - "${_versionNumberFromFile(file)}" + var executor = new IsolateExecutor(generator, [ + "${versionNumberFromFile(file)}" ], message: { "dryRun": dryRun, "schema": schema.asMap(), diff --git a/lib/db/schema/schema.dart b/lib/src/db/schema/schema.dart similarity index 95% rename from lib/db/schema/schema.dart rename to lib/src/db/schema/schema.dart index 9ff1f98f5..bbe874322 100644 --- a/lib/db/schema/schema.dart +++ b/lib/src/db/schema/schema.dart @@ -1,4 +1,11 @@ -part of aqueduct; +import '../managed/managed.dart'; + +import 'schema_table.dart'; + +export 'migration.dart'; +export 'schema_builder.dart'; +export 'schema_column.dart'; +export 'schema_table.dart'; /// Thrown when a [Schema] encounters an error. class SchemaException implements Exception { @@ -14,7 +21,7 @@ class Schema { Schema(this.tables); Schema.fromDataModel(ManagedDataModel dataModel) { - tables = dataModel._entities.values + tables = dataModel.entities .map((e) => new SchemaTable.fromEntity(e)) .toList(); } diff --git a/lib/db/schema/schema_builder.dart b/lib/src/db/schema/schema_builder.dart similarity index 94% rename from lib/db/schema/schema_builder.dart rename to lib/src/db/schema/schema_builder.dart index 6aa50bb9a..29f4383ce 100644 --- a/lib/db/schema/schema_builder.dart +++ b/lib/src/db/schema/schema_builder.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'schema.dart'; +import '../persistent_store/persistent_store.dart'; /// Used during migration to modify a schema. class SchemaBuilder { @@ -135,9 +136,9 @@ class SchemaBuilder { var newColumn = new SchemaColumn.from(existingColumn); modify(newColumn); - if (existingColumn._type != newColumn._type) { + if (existingColumn.type != newColumn.type) { throw new SchemaException( - "May not change column (${existingColumn.name}) type (${existingColumn._type} -> ${newColumn._type})"); + "May not change column (${existingColumn.name}) type (${existingColumn.typeString} -> ${newColumn.typeString})"); } if (existingColumn.autoincrement != newColumn.autoincrement) { @@ -171,7 +172,7 @@ class SchemaBuilder { "May not change column (${existingColumn.name}) to be nullable without unencodedInitialValue."); } - table._replaceColumn(existingColumn, newColumn); + table.replaceColumn(existingColumn, newColumn); if (store != null) { if (existingColumn.isIndexed != newColumn.isIndexed) { @@ -195,7 +196,7 @@ class SchemaBuilder { commands.addAll(store.alterColumnDefaultValue(table, newColumn)); } - if (existingColumn._deleteRule != newColumn._deleteRule) { + if (existingColumn.deleteRule != newColumn.deleteRule) { commands.addAll(store.alterColumnDeleteRule(table, newColumn)); } } @@ -248,14 +249,14 @@ class SchemaBuilder { var builder = new StringBuffer(); if (column.relatedTableName != null) { builder.write( - '${spaceOffset}new SchemaColumn.relationship("${column.name}", ${SchemaColumn.typeFromTypeString(column._type)}'); + '${spaceOffset}new SchemaColumn.relationship("${column.name}", ${column.type}'); builder.write(", relatedTableName: \"${column.relatedTableName}\""); builder.write(", relatedColumnName: \"${column.relatedColumnName}\""); builder.write( - ", rule: ${SchemaColumn.deleteRuleForDeleteRuleString(column._deleteRule)}"); + ", rule: ${column.deleteRule}"); } else { builder.write( - '${spaceOffset}new SchemaColumn("${column.name}", ${SchemaColumn.typeFromTypeString(column._type)}'); + '${spaceOffset}new SchemaColumn("${column.name}", ${column.type}'); if (column.isPrimaryKey) { builder.write(", isPrimaryKey: true"); } else { diff --git a/lib/db/schema/schema_column.dart b/lib/src/db/schema/schema_column.dart similarity index 98% rename from lib/db/schema/schema_column.dart rename to lib/src/db/schema/schema_column.dart index 510d511c2..36ae75748 100644 --- a/lib/db/schema/schema_column.dart +++ b/lib/src/db/schema/schema_column.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'dart:mirrors'; + +import '../managed/managed.dart'; +import 'schema.dart'; /// Represents a database column for a [SchemaTable]. /// @@ -80,6 +83,8 @@ class SchemaColumn { String name; String _type; + String get typeString => _type; + ManagedPropertyType get type => typeFromTypeString(_type); void set type(ManagedPropertyType t) { _type = typeStringForType(t); diff --git a/lib/db/schema/schema_table.dart b/lib/src/db/schema/schema_table.dart similarity index 96% rename from lib/db/schema/schema_table.dart rename to lib/src/db/schema/schema_table.dart index 62b9ba488..35bb1541c 100644 --- a/lib/db/schema/schema_table.dart +++ b/lib/src/db/schema/schema_table.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'schema.dart'; +import '../managed/managed.dart'; /// Represents a database table for a [Schema]. /// @@ -116,7 +117,7 @@ class SchemaTable { columns.remove(column); } - void _replaceColumn(SchemaColumn existingColumn, SchemaColumn newColumn) { + void replaceColumn(SchemaColumn existingColumn, SchemaColumn newColumn) { existingColumn = columnForName(existingColumn.name); if (existingColumn == null) { throw new SchemaException( diff --git a/lib/http/body_decoder.dart b/lib/src/http/body_decoder.dart similarity index 98% rename from lib/http/body_decoder.dart rename to lib/src/http/body_decoder.dart index 6d303658f..a8910f0d2 100644 --- a/lib/http/body_decoder.dart +++ b/lib/src/http/body_decoder.dart @@ -1,4 +1,6 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:convert'; /// A decoding method for decoding a stream of bytes from an HTTP request body into a String. /// diff --git a/lib/http/controller_routing.dart b/lib/src/http/controller_routing.dart similarity index 63% rename from lib/http/controller_routing.dart rename to lib/src/http/controller_routing.dart index 7fed5259d..4b75943a9 100644 --- a/lib/http/controller_routing.dart +++ b/lib/src/http/controller_routing.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'http_controller_internal.dart'; +import 'http_controller.dart'; /// Indicates that an [HTTPController] method is triggered by an HTTP GET method. /// @@ -38,3 +39,30 @@ class HTTPMethod { /// Case-insensitive. final String method; } + +/// Marks a controller HTTPHeader or HTTPQuery property as required. +const HTTPRequiredParameter requiredHTTPParameter = +const HTTPRequiredParameter(); + +class HTTPRequiredParameter { + const HTTPRequiredParameter(); +} + +/// Specifies the route path variable for the associated controller method argument. +class HTTPPath extends HTTPParameter { + const HTTPPath(String segment) : super(segment); +} + +/// Metadata indicating a parameter to a controller's method should be set from +/// the HTTP header indicated by the [header] field. The [header] value is case- +/// insensitive. +class HTTPHeader extends HTTPParameter { + const HTTPHeader(String header) : super(header); +} + +/// Metadata indicating a parameter to a controller's method should be set from +/// the query value (or form-encoded body) from the indicated [key]. The [key] +/// value is case-sensitive. +class HTTPQuery extends HTTPParameter { + const HTTPQuery(String key) : super(key); +} \ No newline at end of file diff --git a/lib/http/cors_policy.dart b/lib/src/http/cors_policy.dart similarity index 99% rename from lib/http/cors_policy.dart rename to lib/src/http/cors_policy.dart index 79f35c19b..9db4e3024 100644 --- a/lib/http/cors_policy.dart +++ b/lib/src/http/cors_policy.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'dart:io'; +import 'http.dart'; /// Describes a CORS policy for a [RequestController]. /// diff --git a/lib/http/documentable.dart b/lib/src/http/documentable.dart similarity index 98% rename from lib/http/documentable.dart rename to lib/src/http/documentable.dart index 1fb3cb51c..1bec614ef 100644 --- a/lib/http/documentable.dart +++ b/lib/src/http/documentable.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:mirrors'; + +import 'package:path/path.dart' as path_lib; Map _stripNull(Map m) { var outMap = {}; @@ -418,19 +421,6 @@ enum APIParameterLocation { query, header, path, formData, cookie, body } /// Represents a parameter in the OpenAPI specification. class APIParameter { - static APIParameterLocation _parameterLocationFromHTTPParameter( - _HTTPParameter p) { - if (p is HTTPPath) { - return APIParameterLocation.path; - } else if (p is HTTPQuery) { - return APIParameterLocation.query; - } else if (p is HTTPHeader) { - return APIParameterLocation.header; - } - - return null; - } - static String parameterLocationStringForType( APIParameterLocation parameterLocation) { switch (parameterLocation) { diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart new file mode 100644 index 000000000..6f467a6d8 --- /dev/null +++ b/lib/src/http/http.dart @@ -0,0 +1,15 @@ +export 'body_decoder.dart'; +export 'controller_routing.dart'; +export 'cors_policy.dart'; +export 'documentable.dart'; +export 'http_controller.dart'; +export 'http_response_exception.dart'; +export 'query_controller.dart'; +export 'request.dart'; +export 'request_controller.dart'; +export 'request_path.dart'; +export 'request_sink.dart'; +export 'resource_controller.dart'; +export 'response.dart'; +export 'router.dart'; +export 'serializable.dart'; \ No newline at end of file diff --git a/lib/http/http_controller.dart b/lib/src/http/http_controller.dart similarity index 83% rename from lib/http/http_controller.dart rename to lib/src/http/http_controller.dart index 18583b27c..6830d875c 100644 --- a/lib/http/http_controller.dart +++ b/lib/src/http/http_controller.dart @@ -1,4 +1,11 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:mirrors'; + +import 'package:analyzer/analyzer.dart'; + +import 'http.dart'; +import 'http_controller_internal.dart'; /// Base class for HTTP web service controller. /// @@ -20,7 +27,7 @@ part of aqueduct; @cannotBeReused abstract class HTTPController extends RequestController { static ContentType _applicationWWWFormURLEncodedContentType = - new ContentType("application", "x-www-form-urlencoded"); + new ContentType("application", "x-www-form-urlencoded"); /// The request being processed by this [HTTPController]. /// @@ -81,14 +88,14 @@ abstract class HTTPController extends RequestController { bool _requestContentTypeIsSupported(Request req) { var incomingContentType = request.innerRequest.headers.contentType; return acceptedContentTypes.firstWhere((ct) { - return ct.primaryType == incomingContentType.primaryType && - ct.subType == incomingContentType.subType; - }, orElse: () => null) != + return ct.primaryType == incomingContentType.primaryType && + ct.subType == incomingContentType.subType; + }, orElse: () => null) != null; } Future _process() async { - var controllerCache = _HTTPControllerCache.cacheForType(runtimeType); + var controllerCache = HTTPControllerCache.cacheForType(runtimeType); var mapper = controllerCache.mapperForRequest(request); if (mapper == null) { return new Response( @@ -124,13 +131,13 @@ abstract class HTTPController extends RequestController { } var orderedParameters = - mapper.positionalParametersFromRequest(request, queryParameters); + mapper.positionalParametersFromRequest(request, queryParameters); var controllerProperties = controllerCache.propertiesFromRequest( request.innerRequest.headers, queryParameters); var missingParameters = [orderedParameters, controllerProperties.values] .expand((p) => p) - .where((p) => p is _HTTPControllerMissingParameter) - .map((p) => p as _HTTPControllerMissingParameter) + .where((p) => p is HTTPControllerMissingParameter) + .map((p) => p as HTTPControllerMissingParameter) .toList(); if (missingParameters.length > 0) { return _missingRequiredParameterResponseIfNecessary(missingParameters); @@ -141,10 +148,10 @@ abstract class HTTPController extends RequestController { Future eventualResponse = reflect(this) .invoke( - mapper.methodSymbol, - orderedParameters, - mapper.optionalParametersFromRequest( - request.innerRequest.headers, queryParameters)) + mapper.methodSymbol, + orderedParameters, + mapper.optionalParametersFromRequest( + request.innerRequest.headers, queryParameters)) .reflectee as Future; var response = await eventualResponse; @@ -174,14 +181,14 @@ abstract class HTTPController extends RequestController { } return response; - } on _InternalControllerException catch (e) { + } on InternalControllerException catch (e) { return e.response; } } @override List documentOperations(PackagePathResolver resolver) { - var controllerCache = _HTTPControllerCache.cacheForType(runtimeType); + var controllerCache = HTTPControllerCache.cacheForType(runtimeType); var reflectedType = reflect(this).type; var uri = reflectedType.location.sourceUri; var fileUnit = parseDartFile(resolver.resolve(uri)); @@ -215,7 +222,7 @@ abstract class HTTPController extends RequestController { var comment = methodDeclaration.documentationComment; var tokens = comment?.tokens ?? []; var lines = - tokens.map((t) => t.lexeme.trimLeft().substring(3).trim()).toList(); + tokens.map((t) => t.lexeme.trimLeft().substring(3).trim()).toList(); if (lines.length > 0) { op.summary = lines.first; } @@ -227,7 +234,7 @@ abstract class HTTPController extends RequestController { bool usesFormEncodedData = op.method.toLowerCase() == "post" && acceptedContentTypes.any((ct) => - ct.primaryType == "application" && + ct.primaryType == "application" && ct.subType == "x-www-form-urlencoded"); op.parameters = [ @@ -235,8 +242,7 @@ abstract class HTTPController extends RequestController { cachedMethod.optionalParameters.values, controllerCache.propertyCache.values ].expand((i) => i.toList()).map((param) { - var paramLocation = APIParameter - ._parameterLocationFromHTTPParameter(param.httpParameter); + var paramLocation = _parameterLocationFromHTTPParameter(param.httpParameter); if (usesFormEncodedData && paramLocation == APIParameterLocation.query) { paramLocation = APIParameterLocation.formData; @@ -247,7 +253,7 @@ abstract class HTTPController extends RequestController { ..required = param.isRequired ..parameterLocation = paramLocation ..schemaObject = - (new APISchemaObject.fromTypeMirror(param.typeMirror)); + (new APISchemaObject.fromTypeMirror(param.typeMirror)); }).toList(); return op; @@ -266,7 +272,7 @@ abstract class HTTPController extends RequestController { var symbol = APIOperation.symbolForID(operation.id, this); if (symbol != null) { - var controllerCache = _HTTPControllerCache.cacheForType(runtimeType); + var controllerCache = HTTPControllerCache.cacheForType(runtimeType); var methodMirror = reflect(this).type.declarations[symbol]; if (controllerCache.hasRequiredParametersForMethod(methodMirror)) { @@ -282,46 +288,20 @@ abstract class HTTPController extends RequestController { } } -class _InternalControllerException implements Exception { - final String message; - final int statusCode; - final HttpHeaders additionalHeaders; - final String responseMessage; - - _InternalControllerException(this.message, this.statusCode, - {HttpHeaders additionalHeaders: null, String responseMessage: null}) - : this.additionalHeaders = additionalHeaders, - this.responseMessage = responseMessage; - - Response get response { - var headerMap = {}; - additionalHeaders?.forEach((k, _) { - headerMap[k] = additionalHeaders.value(k); - }); - - var bodyMap = null; - if (responseMessage != null) { - bodyMap = {"error": responseMessage}; - } - return new Response(statusCode, headerMap, bodyMap); - } -} - -Response _missingRequiredParameterResponseIfNecessary( - List<_HTTPControllerMissingParameter> params) { +Response _missingRequiredParameterResponseIfNecessary(List params) { var missingHeaders = params - .where((p) => p.type == _HTTPControllerMissingParameterType.header) + .where((p) => p.type == HTTPControllerMissingParameterType.header) .map((p) => p.externalName) .toList(); var missingQueryParameters = params - .where((p) => p.type == _HTTPControllerMissingParameterType.query) + .where((p) => p.type == HTTPControllerMissingParameterType.query) .map((p) => p.externalName) .toList(); StringBuffer missings = new StringBuffer(); if (missingQueryParameters.isNotEmpty) { var missingQueriesString = - missingQueryParameters.map((p) => "'${p}'").join(", "); + missingQueryParameters.map((p) => "'${p}'").join(", "); missings.write("Missing query value(s): ${missingQueriesString}."); } if (missingQueryParameters.isNotEmpty && missingHeaders.isNotEmpty) { @@ -334,3 +314,15 @@ Response _missingRequiredParameterResponseIfNecessary( return new Response.badRequest(body: {"error": missings.toString()}); } + +APIParameterLocation _parameterLocationFromHTTPParameter(HTTPParameter p) { + if (p is HTTPPath) { + return APIParameterLocation.path; + } else if (p is HTTPQuery) { + return APIParameterLocation.query; + } else if (p is HTTPHeader) { + return APIParameterLocation.header; + } + + return null; +} \ No newline at end of file diff --git a/lib/http/parameter_matching.dart b/lib/src/http/http_controller_internal.dart similarity index 54% rename from lib/http/parameter_matching.dart rename to lib/src/http/http_controller_internal.dart index 445bffb57..9ec8ecc53 100644 --- a/lib/http/parameter_matching.dart +++ b/lib/src/http/http_controller_internal.dart @@ -1,69 +1,73 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:mirrors'; + +import 'controller_routing.dart'; +import 'request.dart'; +import 'response.dart'; + +class InternalControllerException implements Exception { + final String message; + final int statusCode; + final HttpHeaders additionalHeaders; + final String responseMessage; + + InternalControllerException(this.message, this.statusCode, + {HttpHeaders additionalHeaders: null, String responseMessage: null}) + : this.additionalHeaders = additionalHeaders, + this.responseMessage = responseMessage; + + Response get response { + var headerMap = {}; + additionalHeaders?.forEach((k, _) { + headerMap[k] = additionalHeaders.value(k); + }); + + var bodyMap = null; + if (responseMessage != null) { + bodyMap = {"error": responseMessage}; + } + return new Response(statusCode, headerMap, bodyMap); + } +} /// Parent class for annotations used for optional parameters in controller methods -abstract class _HTTPParameter { - const _HTTPParameter(this.externalName); +abstract class HTTPParameter { + const HTTPParameter(this.externalName); /// The name of the variable in the HTTP request. final String externalName; } -/// Marks a controller HTTPHeader or HTTPQuery property as required. -const HTTPRequiredParameter requiredHTTPParameter = - const HTTPRequiredParameter(); - -class HTTPRequiredParameter { - const HTTPRequiredParameter(); -} - -/// Specifies the route path variable for the associated controller method argument. -class HTTPPath extends _HTTPParameter { - const HTTPPath(String segment) : super(segment); -} - -/// Metadata indicating a parameter to a controller's method should be set from -/// the HTTP header indicated by the [header] field. The [header] value is case- -/// insensitive. -class HTTPHeader extends _HTTPParameter { - const HTTPHeader(String header) : super(header); -} -/// Metadata indicating a parameter to a controller's method should be set from -/// the query value (or form-encoded body) from the indicated [key]. The [key] -/// value is case-sensitive. -class HTTPQuery extends _HTTPParameter { - const HTTPQuery(String key) : super(key); -} - -class _HTTPControllerCache { - static Map controllerCache = {}; - static _HTTPControllerCache cacheForType(Type t) { +class HTTPControllerCache { + static Map controllerCache = {}; + static HTTPControllerCache cacheForType(Type t) { var cacheItem = controllerCache[t]; if (cacheItem != null) { return cacheItem; } - controllerCache[t] = new _HTTPControllerCache(t); + controllerCache[t] = new HTTPControllerCache(t); return controllerCache[t]; } - _HTTPControllerCache(Type controllerType) { + HTTPControllerCache(Type controllerType) { var allDeclarations = reflectClass(controllerType).declarations; allDeclarations.values .where((decl) => decl is VariableMirror) .where( - (decl) => decl.metadata.any((im) => im.reflectee is _HTTPParameter)) + (decl) => decl.metadata.any((im) => im.reflectee is HTTPParameter)) .forEach((decl) { - _HTTPControllerCachedParameter param; + HTTPControllerCachedParameter param; var isRequired = allDeclarations[decl.simpleName] .metadata .any((im) => im.reflectee is HTTPRequiredParameter); if (isRequired) { hasControllerRequiredParameter = true; - param = new _HTTPControllerCachedParameter(decl, isRequired: true); + param = new HTTPControllerCachedParameter(decl, isRequired: true); } else { - param = new _HTTPControllerCachedParameter(decl, isRequired: false); + param = new HTTPControllerCachedParameter(decl, isRequired: false); } propertyCache[param.symbol] = param; @@ -72,17 +76,17 @@ class _HTTPControllerCache { allDeclarations.values .where((decl) => decl is MethodMirror) .where((decl) => decl.metadata.any((im) => im.reflectee is HTTPMethod)) - .map((decl) => new _HTTPControllerCachedMethod(decl)) - .forEach((_HTTPControllerCachedMethod method) { - var key = _HTTPControllerCachedMethod.generateRequestMethodKey( + .map((decl) => new HTTPControllerCachedMethod(decl)) + .forEach((HTTPControllerCachedMethod method) { + var key = HTTPControllerCachedMethod.generateRequestMethodKey( method.httpMethod.method, method.pathParameters.length); methodCache[key] = method; }); } - Map methodCache = {}; - Map propertyCache = {}; + Map methodCache = {}; + Map propertyCache = {}; bool hasControllerRequiredParameter = false; bool hasRequiredParametersForMethod(MethodMirror mm) { @@ -92,8 +96,8 @@ class _HTTPControllerCache { if (mm is MethodMirror && mm.metadata.any((im) => im.reflectee is HTTPMethod)) { - _HTTPControllerCachedMethod method = new _HTTPControllerCachedMethod(mm); - var key = _HTTPControllerCachedMethod.generateRequestMethodKey( + HTTPControllerCachedMethod method = new HTTPControllerCachedMethod(mm); + var key = HTTPControllerCachedMethod.generateRequestMethodKey( method.httpMethod.method, method.pathParameters.length); return methodCache[key] @@ -104,8 +108,8 @@ class _HTTPControllerCache { return false; } - _HTTPControllerCachedMethod mapperForRequest(Request req) { - var key = _HTTPControllerCachedMethod.generateRequestMethodKey( + HTTPControllerCachedMethod mapperForRequest(Request req) { + var key = HTTPControllerCachedMethod.generateRequestMethodKey( req.innerRequest.method, req.path.orderedVariableNames.length); return methodCache[key]; @@ -113,7 +117,7 @@ class _HTTPControllerCache { Map propertiesFromRequest( HttpHeaders headers, Map> queryParameters) { - return _parseParametersFromRequest(propertyCache, headers, queryParameters); + return parseParametersFromRequest(propertyCache, headers, queryParameters); } List allowedMethodsForArity(int arity) { @@ -124,66 +128,66 @@ class _HTTPControllerCache { } } -class _HTTPControllerCachedMethod { +class HTTPControllerCachedMethod { static String generateRequestMethodKey(String httpMethod, int arity) { return "${httpMethod.toLowerCase()}/$arity"; } - _HTTPControllerCachedMethod(MethodMirror mirror) { + HTTPControllerCachedMethod(MethodMirror mirror) { httpMethod = mirror.metadata.firstWhere((m) => m.reflectee is HTTPMethod).reflectee; methodSymbol = mirror.simpleName; positionalParameters = mirror.parameters .where((pm) => !pm.isOptional) - .map((pm) => new _HTTPControllerCachedParameter(pm, isRequired: true)) + .map((pm) => new HTTPControllerCachedParameter(pm, isRequired: true)) .toList(); optionalParameters = new Map.fromIterable( mirror.parameters.where((pm) => pm.isOptional).map( - (pm) => new _HTTPControllerCachedParameter(pm, isRequired: false)), - key: (_HTTPControllerCachedParameter p) => p.symbol, + (pm) => new HTTPControllerCachedParameter(pm, isRequired: false)), + key: (HTTPControllerCachedParameter p) => p.symbol, value: (p) => p); } Symbol methodSymbol; HTTPMethod httpMethod; - List<_HTTPControllerCachedParameter> positionalParameters = []; - Map optionalParameters = {}; - List<_HTTPControllerCachedParameter> get pathParameters => + List positionalParameters = []; + Map optionalParameters = {}; + List get pathParameters => positionalParameters.where((p) => p.httpParameter is HTTPPath).toList(); List positionalParametersFromRequest( Request req, Map> queryParameters) { return positionalParameters.map((param) { if (param.httpParameter is HTTPPath) { - return _convertParameterWithMirror( + return convertParameterWithMirror( req.path.variables[param.name], param.typeMirror); } else if (param.httpParameter is HTTPQuery) { - return _convertParameterListWithMirror( - queryParameters[param.name], param.typeMirror) ?? - new _HTTPControllerMissingParameter( - _HTTPControllerMissingParameterType.query, param.name); + return convertParameterListWithMirror( + queryParameters[param.name], param.typeMirror) ?? + new HTTPControllerMissingParameter( + HTTPControllerMissingParameterType.query, param.name); } else if (param.httpParameter is HTTPHeader) { - return _convertParameterListWithMirror( - req.innerRequest.headers[param.name], param.typeMirror) ?? - new _HTTPControllerMissingParameter( - _HTTPControllerMissingParameterType.header, param.name); + return convertParameterListWithMirror( + req.innerRequest.headers[param.name], param.typeMirror) ?? + new HTTPControllerMissingParameter( + HTTPControllerMissingParameterType.header, param.name); } }).toList(); } Map optionalParametersFromRequest( HttpHeaders headers, Map> queryParameters) { - return _parseParametersFromRequest( + return parseParametersFromRequest( optionalParameters, headers, queryParameters); } } -class _HTTPControllerCachedParameter { - _HTTPControllerCachedParameter(VariableMirror mirror, +class HTTPControllerCachedParameter { + HTTPControllerCachedParameter(VariableMirror mirror, {this.isRequired: false}) { symbol = mirror.simpleName; httpParameter = mirror.metadata - .firstWhere((im) => im.reflectee is _HTTPParameter, orElse: () => null) + .firstWhere((im) => im.reflectee is HTTPParameter, orElse: () => null) ?.reflectee; typeMirror = mirror.type; } @@ -191,21 +195,21 @@ class _HTTPControllerCachedParameter { Symbol symbol; String get name => httpParameter.externalName; TypeMirror typeMirror; - _HTTPParameter httpParameter; + HTTPParameter httpParameter; bool isRequired; } -enum _HTTPControllerMissingParameterType { header, query } +enum HTTPControllerMissingParameterType { header, query } -class _HTTPControllerMissingParameter { - _HTTPControllerMissingParameter(this.type, this.externalName); +class HTTPControllerMissingParameter { + HTTPControllerMissingParameter(this.type, this.externalName); - _HTTPControllerMissingParameterType type; + HTTPControllerMissingParameterType type; String externalName; } -Map _parseParametersFromRequest( - Map mappings, +Map parseParametersFromRequest( + Map mappings, HttpHeaders headers, Map> queryParameters) { return mappings.keys.fold({}, (m, sym) { @@ -214,17 +218,17 @@ Map _parseParametersFromRequest( var paramType = null; if (mapper.httpParameter is HTTPQuery) { - paramType = _HTTPControllerMissingParameterType.query; + paramType = HTTPControllerMissingParameterType.query; value = queryParameters[mapper.httpParameter.externalName]; } else if (mapper.httpParameter is HTTPHeader) { - paramType = _HTTPControllerMissingParameterType.header; + paramType = HTTPControllerMissingParameterType.header; value = headers[mapper.httpParameter.externalName]; } if (value != null) { - m[sym] = _convertParameterListWithMirror(value, mapper.typeMirror); + m[sym] = convertParameterListWithMirror(value, mapper.typeMirror); } else if (mapper.isRequired) { - m[sym] = new _HTTPControllerMissingParameter( + m[sym] = new HTTPControllerMissingParameter( paramType, mapper.httpParameter.externalName); } @@ -232,7 +236,7 @@ Map _parseParametersFromRequest( }); } -dynamic _convertParameterListWithMirror( +dynamic convertParameterListWithMirror( List parameterValues, TypeMirror typeMirror) { if (parameterValues == null) { return null; @@ -241,14 +245,14 @@ dynamic _convertParameterListWithMirror( if (typeMirror.isSubtypeOf(reflectType(List))) { return parameterValues .map((str) => - _convertParameterWithMirror(str, typeMirror.typeArguments.first)) + convertParameterWithMirror(str, typeMirror.typeArguments.first)) .toList(); } else { - return _convertParameterWithMirror(parameterValues.first, typeMirror); + return convertParameterWithMirror(parameterValues.first, typeMirror); } } -dynamic _convertParameterWithMirror( +dynamic convertParameterWithMirror( String parameterValue, TypeMirror typeMirror) { if (parameterValue == null) { return null; @@ -267,10 +271,10 @@ dynamic _convertParameterWithMirror( if (parseDecl != null) { try { var reflValue = - typeMirror.invoke(parseDecl.simpleName, [parameterValue]); + typeMirror.invoke(parseDecl.simpleName, [parameterValue]); return reflValue.reflectee; } catch (e) { - throw new _InternalControllerException( + throw new InternalControllerException( "Invalid value for parameter type", HttpStatus.BAD_REQUEST, responseMessage: "URI parameter is wrong type"); } @@ -278,8 +282,9 @@ dynamic _convertParameterWithMirror( } // If we get here, then it wasn't a string and couldn't be parsed, and we should throw? - throw new _InternalControllerException( + throw new InternalControllerException( "Invalid path parameter type, types must be String or implement parse", HttpStatus.INTERNAL_SERVER_ERROR, responseMessage: "URI parameter is wrong type"); } + diff --git a/lib/http/http_response_exception.dart b/lib/src/http/http_response_exception.dart similarity index 89% rename from lib/http/http_response_exception.dart rename to lib/src/http/http_response_exception.dart index 1ce0905a2..238bf38f9 100644 --- a/lib/http/http_response_exception.dart +++ b/lib/src/http/http_response_exception.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:io'; + +import 'response.dart'; +import 'request_controller.dart'; +import 'request.dart'; /// An exception for early-exiting a [RequestController] to respond to a request. /// diff --git a/lib/http/query_controller.dart b/lib/src/http/query_controller.dart similarity index 97% rename from lib/http/query_controller.dart rename to lib/src/http/query_controller.dart index eabe8699a..e0c6fb146 100644 --- a/lib/http/query_controller.dart +++ b/lib/src/http/query_controller.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:mirrors'; + +import '../db/db.dart'; +import 'http.dart'; /// A partial class for implementing an [HTTPController] that has a few conveniences /// for executing [Query]s. diff --git a/lib/http/request.dart b/lib/src/http/request.dart similarity index 98% rename from lib/http/request.dart rename to lib/src/http/request.dart index 76891882a..30c3a8a48 100644 --- a/lib/http/request.dart +++ b/lib/src/http/request.dart @@ -1,4 +1,10 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +import '../auth/auth.dart'; +import 'http.dart'; /// A single HTTP request. /// diff --git a/lib/http/request_controller.dart b/lib/src/http/request_controller.dart similarity index 96% rename from lib/http/request_controller.dart rename to lib/src/http/request_controller.dart index 346825b9a..3a68ad30b 100644 --- a/lib/http/request_controller.dart +++ b/lib/src/http/request_controller.dart @@ -1,4 +1,12 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:mirrors'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +import 'http.dart'; +import '../application/application.dart'; +import '../db/db.dart'; /// The unifying protocol for [Request] and [Response] classes. /// @@ -153,7 +161,7 @@ class RequestController extends Object with APIDocumentable { if (result is Request && nextController != null) { nextController.receive(req); } else if (result is Response) { - _applyCORSHeadersIfNecessary(req, result); + applyCORSHeadersIfNecessary(req, result); req.respond(result); logger.info(req.toDebugString()); @@ -183,7 +191,7 @@ class RequestController extends Object with APIDocumentable { try { if (caughtValue is HTTPResponseException) { var response = caughtValue.response; - _applyCORSHeadersIfNecessary(request, response); + applyCORSHeadersIfNecessary(request, response); request.respond(response); logger.info( @@ -209,7 +217,7 @@ class RequestController extends Object with APIDocumentable { var response = new Response(statusCode, null, {"error": caughtValue.toString()}); - _applyCORSHeadersIfNecessary(request, response); + applyCORSHeadersIfNecessary(request, response); request.respond(response); logger.info( @@ -226,7 +234,7 @@ class RequestController extends Object with APIDocumentable { var response = new Response.serverError(body: body) ..contentType = ContentType.JSON; - _applyCORSHeadersIfNecessary(request, response); + applyCORSHeadersIfNecessary(request, response); request.respond(response); logger.severe( @@ -245,7 +253,7 @@ class RequestController extends Object with APIDocumentable { return controller; } - void _applyCORSHeadersIfNecessary(Request req, Response resp) { + void applyCORSHeadersIfNecessary(Request req, Response resp) { if (req.isCORSRequest && !req.isPreflightRequest) { var lastPolicyController = _lastRequestController(); var p = lastPolicyController.policy; diff --git a/lib/http/request_path.dart b/lib/src/http/request_path.dart similarity index 99% rename from lib/http/request_path.dart rename to lib/src/http/request_path.dart index 46856a4c1..d0187afb1 100644 --- a/lib/http/request_path.dart +++ b/lib/src/http/request_path.dart @@ -1,4 +1,4 @@ -part of aqueduct; +import 'http.dart'; /// The HTTP request path decomposed into variables and segments based on a [RouteSpecification]. /// diff --git a/lib/http/request_sink.dart b/lib/src/http/request_sink.dart similarity index 97% rename from lib/http/request_sink.dart rename to lib/src/http/request_sink.dart index 84b8f87a8..8e1f0c349 100644 --- a/lib/http/request_sink.dart +++ b/lib/src/http/request_sink.dart @@ -1,4 +1,11 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:async'; + +import 'request.dart'; +import 'request_controller.dart'; +import 'documentable.dart'; +import 'router.dart'; +import '../application/application.dart'; /// Instances of this class are responsible for setting up routing and resources used by an [Application]. /// diff --git a/lib/http/resource_controller.dart b/lib/src/http/resource_controller.dart similarity index 99% rename from lib/http/resource_controller.dart rename to lib/src/http/resource_controller.dart index f497a51e0..3c72a90a9 100644 --- a/lib/http/resource_controller.dart +++ b/lib/src/http/resource_controller.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import '../db/db.dart'; +import 'http.dart'; + /// A [RequestController] for performing CRUD operations on [ManagedObject] instances. /// diff --git a/lib/http/response.dart b/lib/src/http/response.dart similarity index 99% rename from lib/http/response.dart rename to lib/src/http/response.dart index a856a78c6..39b48c80f 100644 --- a/lib/http/response.dart +++ b/lib/src/http/response.dart @@ -1,4 +1,6 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:convert'; +import 'http.dart'; /// Represents the information in an HTTP response. /// diff --git a/lib/http/route_node.dart b/lib/src/http/route_node.dart similarity index 85% rename from lib/http/route_node.dart rename to lib/src/http/route_node.dart index dc52b26d3..56980d3af 100644 --- a/lib/http/route_node.dart +++ b/lib/src/http/route_node.dart @@ -1,7 +1,7 @@ -part of aqueduct; +import 'http.dart'; -class _RouteNode { - _RouteNode(List specs, +class RouteNode { + RouteNode(List specs, {int level: 0, RegExp matcher: null}) { patternMatcher = matcher; @@ -23,7 +23,7 @@ class _RouteNode { var literalMatcher = (RouteSpecification spec) => spec.segments[level].literal == segmentLiteral; - literalChildren[segmentLiteral] = new _RouteNode( + literalChildren[segmentLiteral] = new RouteNode( specs.where(literalMatcher).toList(), level: level + 1); specs.removeWhere(literalMatcher); @@ -33,7 +33,7 @@ class _RouteNode { (rps) => rps.segments[level].isRemainingMatcher, orElse: () => null); if (anyMatcher != null) { - anyMatcherChildNode = new _RouteNode.withSpecification(anyMatcher); + anyMatcherChildNode = new RouteNode.withSpecification(anyMatcher); specs.removeWhere((rps) => rps.segments[level].isRemainingMatcher); } @@ -49,23 +49,23 @@ class _RouteNode { "Cannot disambiguate from the following routes, as one of them will match anything: $matchingSpecs"); } - return new _RouteNode(matchingSpecs, + return new RouteNode(matchingSpecs, level: level + 1, matcher: matchingSpecs.first.segments[level].matcher); }).toList(); } - _RouteNode.withSpecification(this.specification); + RouteNode.withSpecification(this.specification); bool matchingAnything = false; RegExp patternMatcher; RequestController get controller => specification?.controller; RouteSpecification specification; - List<_RouteNode> patternMatchChildren = []; - Map literalChildren = {}; - _RouteNode anyMatcherChildNode; + List patternMatchChildren = []; + Map literalChildren = {}; + RouteNode anyMatcherChildNode; - _RouteNode nodeForPathSegments(List requestSegments) { + RouteNode nodeForPathSegments(List requestSegments) { if (requestSegments.isEmpty) { return this; } diff --git a/lib/http/router.dart b/lib/src/http/router.dart similarity index 96% rename from lib/http/router.dart rename to lib/src/http/router.dart index 471b35efd..5d7d4f03c 100644 --- a/lib/http/router.dart +++ b/lib/src/http/router.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'dart:async'; + +import 'http.dart'; +import 'route_node.dart'; /// A router to split requests based on their URI path. /// @@ -23,7 +26,7 @@ class Router extends RequestController { } List _routeControllers = []; - _RouteNode _rootRouteNode; + RouteNode _rootRouteNode; /// A string to be prepended to the beginning of every route this [Router] manages. /// @@ -81,7 +84,7 @@ class Router extends RequestController { /// you must call this method after all routes have been added to build a tree of routes for optimized route finding. void finalize() { _rootRouteNode = - new _RouteNode(_routeControllers.expand((rh) => rh.patterns).toList()); + new RouteNode(_routeControllers.expand((rh) => rh.patterns).toList()); } /// Routers override this method to throw an exception. Use [route] instead. @@ -146,7 +149,7 @@ class Router extends RequestController { void _handleUnhandledRequest(Request req) { var response = new Response.notFound(); - _applyCORSHeadersIfNecessary(req, response); + applyCORSHeadersIfNecessary(req, response); req.respond(response); logger.info("${req.toDebugString()}"); } diff --git a/lib/http/serializable.dart b/lib/src/http/serializable.dart similarity index 96% rename from lib/http/serializable.dart rename to lib/src/http/serializable.dart index e5c6d044d..b5cda9a56 100644 --- a/lib/http/serializable.dart +++ b/lib/src/http/serializable.dart @@ -1,4 +1,4 @@ -part of aqueduct; + /// Interface for serializable instances to be returned as the HTTP response body. /// diff --git a/lib/utilities/mirror_helpers.dart b/lib/src/utilities/mirror_helpers.dart similarity index 90% rename from lib/utilities/mirror_helpers.dart rename to lib/src/utilities/mirror_helpers.dart index 7bbf5a5ff..afc6a6012 100644 --- a/lib/utilities/mirror_helpers.dart +++ b/lib/src/utilities/mirror_helpers.dart @@ -1,6 +1,8 @@ import 'dart:mirrors'; -import '../aqueduct.dart'; +import '../db/managed/object.dart'; +import '../db/managed/set.dart'; +import '../db/managed/attributes.dart'; bool doesVariableMirrorRepresentRelationship(VariableMirror mirror) { var modelMirror = reflectType(ManagedObject); diff --git a/lib/utilities/mock_server.dart b/lib/src/utilities/mock_server.dart similarity index 98% rename from lib/utilities/mock_server.dart rename to lib/src/utilities/mock_server.dart index fcf80358e..37055d759 100644 --- a/lib/utilities/mock_server.dart +++ b/lib/src/utilities/mock_server.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:convert'; + +import '../http/request.dart'; +import '../http/response.dart'; /// This class is used as a utility for testing. /// diff --git a/lib/utilities/pbkdf2.dart b/lib/src/utilities/pbkdf2.dart similarity index 97% rename from lib/utilities/pbkdf2.dart rename to lib/src/utilities/pbkdf2.dart index 35956b7c6..23d9a0d5e 100644 --- a/lib/utilities/pbkdf2.dart +++ b/lib/src/utilities/pbkdf2.dart @@ -1,4 +1,4 @@ -part of aqueduct; + /* Based on implementation found here: https://github.com/jamesots/pbkdf2, which contains the following license: @@ -17,6 +17,10 @@ part of aqueduct; limitations under the License. */ +import 'dart:math'; + +import 'package:crypto/crypto.dart'; + /// Instances of this type perform one-way cryptographic hashing using the PBKDF2 algorithm. class PBKDF2 { Hash hashAlgorithm; diff --git a/lib/utilities/source_generator.dart b/lib/src/utilities/source_generator.dart similarity index 89% rename from lib/utilities/source_generator.dart rename to lib/src/utilities/source_generator.dart index 3ba29a706..9cbc62bab 100644 --- a/lib/utilities/source_generator.dart +++ b/lib/src/utilities/source_generator.dart @@ -1,14 +1,18 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'token_generator.dart'; +import 'dart:mirrors'; -class _SourceGenerator { +class SourceGenerator { static String generate(Function closure, {List imports: const [], String additionalContents}) { - var gen = new _SourceGenerator(closure, + var gen = new SourceGenerator(closure, imports: imports, additionalContents: additionalContents); return gen.source; } - _SourceGenerator(this.closure, + SourceGenerator(this.closure, {this.imports: const [], this.additionalContents}); Function closure; @@ -45,11 +49,11 @@ class _SourceGenerator { } } -class _IsolateExecutor { - _IsolateExecutor(this.generator, this.arguments, +class IsolateExecutor { + IsolateExecutor(this.generator, this.arguments, {this.message, this.packageConfigURI}); - _SourceGenerator generator; + SourceGenerator generator; Map message; List arguments; Uri packageConfigURI; diff --git a/lib/utilities/test_client.dart b/lib/src/utilities/test_client.dart similarity index 98% rename from lib/utilities/test_client.dart rename to lib/src/utilities/test_client.dart index 1486f1f27..5c4322014 100644 --- a/lib/utilities/test_client.dart +++ b/lib/src/utilities/test_client.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:async'; +import 'dart:convert'; +import '../application/application.dart'; +import '../application/application_configuration.dart'; + /// Instances of this class are used during testing to make testing an HTTP server more convenient. /// diff --git a/lib/utilities/test_matchers.dart b/lib/src/utilities/test_matchers.dart similarity index 99% rename from lib/utilities/test_matchers.dart rename to lib/src/utilities/test_matchers.dart index 38a530ed4..30e6789ff 100644 --- a/lib/utilities/test_matchers.dart +++ b/lib/src/utilities/test_matchers.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'package:matcher/matcher.dart'; +import 'test_client.dart'; /// Validates that expected result is a [num]. const Matcher isNumber = const isInstanceOf(); diff --git a/lib/utilities/token_generator.dart b/lib/src/utilities/token_generator.dart similarity index 100% rename from lib/utilities/token_generator.dart rename to lib/src/utilities/token_generator.dart diff --git a/test/auth/auth_controller_test.dart b/test/auth/auth_controller_test.dart index d2ca303b9..1272d26c0 100644 --- a/test/auth/auth_controller_test.dart +++ b/test/auth/auth_controller_test.dart @@ -5,6 +5,8 @@ import 'dart:convert'; import '../helpers.dart'; void main() { + RequestController.includeErrorDetailsInServerErrorResponses = true; + ManagedContext context = null; HttpServer server; TestClient client = new TestClient.onPort(8080) diff --git a/test/helpers.dart b/test/helpers.dart index dd94308d8..83062a6d4 100644 --- a/test/helpers.dart +++ b/test/helpers.dart @@ -121,6 +121,7 @@ class AuthDelegate implements AuthServerDelegate { tokenQ.predicate = new QueryPredicate( "refreshToken = @refreshToken", {"refreshToken": t.refreshToken}); tokenQ.values = t; + return tokenQ.updateOne(); } @@ -146,6 +147,7 @@ class AuthDelegate implements AuthServerDelegate { Future deleteAuthCode(AuthServer server, AuthCode code) async { var authCodeQ = new Query(); authCodeQ.predicate = new QueryPredicate("id = @id", {"id": code.id}); + return authCodeQ.delete(); } From 49488545673b03a67ed6fedd72090d7402b1f3a1 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Sun, 27 Nov 2016 21:09:34 -0500 Subject: [PATCH 09/36] Updated test/template projects to use import instead of part, changed data model builder to include lib dependencies --- .../src/controller/identity_controller.dart | 2 +- .../src/controller/register_controller.dart | 2 +- .../lib/src/controller/user_controller.dart | 2 +- .../default/lib/src/model/token.dart | 2 +- .../templates/default/lib/src/model/user.dart | 2 +- .../lib/src/utilities/auth_delegate.dart | 2 +- .../default/lib/src/wildfire_sink.dart | 2 +- example/templates/default/lib/wildfire.dart | 19 ++++++++---------- lib/src/db/managed/data_model.dart | 9 ++++++++- .../src/controller/identity_controller.dart | 2 +- .../src/controller/register_controller.dart | 2 +- .../lib/src/controller/user_controller.dart | 2 +- test/test_project/lib/src/model/token.dart | 2 +- test/test_project/lib/src/model/user.dart | 2 +- .../lib/src/utilities/auth_delegate.dart | 2 +- test/test_project/lib/src/wildfire_sink.dart | 2 +- test/test_project/lib/wildfire.dart | 20 +++++++++---------- 17 files changed, 39 insertions(+), 37 deletions(-) diff --git a/example/templates/default/lib/src/controller/identity_controller.dart b/example/templates/default/lib/src/controller/identity_controller.dart index b5359a27f..1eb068315 100644 --- a/example/templates/default/lib/src/controller/identity_controller.dart +++ b/example/templates/default/lib/src/controller/identity_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class IdentityController extends HTTPController { @httpGet diff --git a/example/templates/default/lib/src/controller/register_controller.dart b/example/templates/default/lib/src/controller/register_controller.dart index ae3b9dab4..7956e2437 100644 --- a/example/templates/default/lib/src/controller/register_controller.dart +++ b/example/templates/default/lib/src/controller/register_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class RegisterController extends QueryController { @httpPost diff --git a/example/templates/default/lib/src/controller/user_controller.dart b/example/templates/default/lib/src/controller/user_controller.dart index bc775ba7b..f7f523249 100644 --- a/example/templates/default/lib/src/controller/user_controller.dart +++ b/example/templates/default/lib/src/controller/user_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class UserController extends QueryController { @httpGet diff --git a/example/templates/default/lib/src/model/token.dart b/example/templates/default/lib/src/model/token.dart index ebf746f91..56f174df4 100644 --- a/example/templates/default/lib/src/model/token.dart +++ b/example/templates/default/lib/src/model/token.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class AuthCode extends ManagedObject<_AuthCode> implements _AuthCode {} diff --git a/example/templates/default/lib/src/model/user.dart b/example/templates/default/lib/src/model/user.dart index 04cce11c1..9700edc00 100644 --- a/example/templates/default/lib/src/model/user.dart +++ b/example/templates/default/lib/src/model/user.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class User extends ManagedObject<_User> implements _User, Authenticatable { @managedTransientInputAttribute diff --git a/example/templates/default/lib/src/utilities/auth_delegate.dart b/example/templates/default/lib/src/utilities/auth_delegate.dart index 26c7108a5..0bc378fdc 100644 --- a/example/templates/default/lib/src/utilities/auth_delegate.dart +++ b/example/templates/default/lib/src/utilities/auth_delegate.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class WildfireAuthenticationDelegate implements AuthServerDelegate { diff --git a/example/templates/default/lib/src/wildfire_sink.dart b/example/templates/default/lib/src/wildfire_sink.dart index 6d3f9d60b..fa89735f8 100644 --- a/example/templates/default/lib/src/wildfire_sink.dart +++ b/example/templates/default/lib/src/wildfire_sink.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../wildfire.dart'; class WildfireConfiguration extends ConfigurationItem { WildfireConfiguration(String fileName) : super.fromFile(fileName); diff --git a/example/templates/default/lib/wildfire.dart b/example/templates/default/lib/wildfire.dart index 059507aa8..2c7932c80 100644 --- a/example/templates/default/lib/wildfire.dart +++ b/example/templates/default/lib/wildfire.dart @@ -6,18 +6,15 @@ /// A web server. library wildfire; -import 'dart:io'; -import 'dart:async'; -import 'package:aqueduct/aqueduct.dart'; -import 'package:scribe/scribe.dart'; +export 'dart:async'; +export 'dart:io'; export 'package:aqueduct/aqueduct.dart'; export 'package:scribe/scribe.dart'; -part 'src/model/token.dart'; -part 'src/model/user.dart'; -part 'src/wildfire_sink.dart'; -part 'src/controller/user_controller.dart'; -part 'src/controller/identity_controller.dart'; -part 'src/controller/register_controller.dart'; -part 'src/utilities/auth_delegate.dart'; +export 'src/model/token.dart'; +export 'src/model/user.dart'; +export 'src/controller/user_controller.dart'; +export 'src/controller/identity_controller.dart'; +export 'src/controller/register_controller.dart'; +export 'src/utilities/auth_delegate.dart'; diff --git a/lib/src/db/managed/data_model.dart b/lib/src/db/managed/data_model.dart index df7fc71bb..89c1b6254 100644 --- a/lib/src/db/managed/data_model.dart +++ b/lib/src/db/managed/data_model.dart @@ -67,8 +67,15 @@ class ManagedDataModel { } List _modelTypesFromLibraryMirror(LibraryMirror libMirror) { + var allLibraries = libMirror.libraryDependencies + .where((dep) => dep.isExport) + .map((dep) => dep.targetLibrary) + .toList(); + allLibraries.add(libMirror); + var modelMirror = reflectClass(ManagedObject); - Iterable allClasses = libMirror.declarations.values + Iterable allClasses = allLibraries + .expand((lm) => lm.declarations.values) .where((decl) => decl is ClassMirror) .map((decl) => decl as ClassMirror); diff --git a/test/test_project/lib/src/controller/identity_controller.dart b/test/test_project/lib/src/controller/identity_controller.dart index b5359a27f..1eb068315 100644 --- a/test/test_project/lib/src/controller/identity_controller.dart +++ b/test/test_project/lib/src/controller/identity_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class IdentityController extends HTTPController { @httpGet diff --git a/test/test_project/lib/src/controller/register_controller.dart b/test/test_project/lib/src/controller/register_controller.dart index ae3b9dab4..7956e2437 100644 --- a/test/test_project/lib/src/controller/register_controller.dart +++ b/test/test_project/lib/src/controller/register_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class RegisterController extends QueryController { @httpPost diff --git a/test/test_project/lib/src/controller/user_controller.dart b/test/test_project/lib/src/controller/user_controller.dart index a89156571..5336dec31 100644 --- a/test/test_project/lib/src/controller/user_controller.dart +++ b/test/test_project/lib/src/controller/user_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class UserController extends QueryController { @httpGet diff --git a/test/test_project/lib/src/model/token.dart b/test/test_project/lib/src/model/token.dart index ebf746f91..56f174df4 100644 --- a/test/test_project/lib/src/model/token.dart +++ b/test/test_project/lib/src/model/token.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class AuthCode extends ManagedObject<_AuthCode> implements _AuthCode {} diff --git a/test/test_project/lib/src/model/user.dart b/test/test_project/lib/src/model/user.dart index 04cce11c1..9700edc00 100644 --- a/test/test_project/lib/src/model/user.dart +++ b/test/test_project/lib/src/model/user.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class User extends ManagedObject<_User> implements _User, Authenticatable { @managedTransientInputAttribute diff --git a/test/test_project/lib/src/utilities/auth_delegate.dart b/test/test_project/lib/src/utilities/auth_delegate.dart index 35b455b47..0e409deab 100644 --- a/test/test_project/lib/src/utilities/auth_delegate.dart +++ b/test/test_project/lib/src/utilities/auth_delegate.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class WildfireAuthenticationDelegate implements AuthServerDelegate { diff --git a/test/test_project/lib/src/wildfire_sink.dart b/test/test_project/lib/src/wildfire_sink.dart index e5ab77f57..92ab9091e 100644 --- a/test/test_project/lib/src/wildfire_sink.dart +++ b/test/test_project/lib/src/wildfire_sink.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../wildfire.dart'; class WildfireConfiguration extends ConfigurationItem { WildfireConfiguration(String fileName) : super.fromFile(fileName); diff --git a/test/test_project/lib/wildfire.dart b/test/test_project/lib/wildfire.dart index b7fddd986..4c6d6d127 100644 --- a/test/test_project/lib/wildfire.dart +++ b/test/test_project/lib/wildfire.dart @@ -6,16 +6,14 @@ /// A web server. library wildfire; -import 'dart:io'; -import 'dart:async'; -import 'package:aqueduct/aqueduct.dart'; - +export 'dart:io'; +export 'dart:async'; export 'package:aqueduct/aqueduct.dart'; -part 'src/model/token.dart'; -part 'src/model/user.dart'; -part 'src/wildfire_sink.dart'; -part 'src/controller/user_controller.dart'; -part 'src/controller/identity_controller.dart'; -part 'src/controller/register_controller.dart'; -part 'src/utilities/auth_delegate.dart'; +export 'src/model/token.dart'; +export 'src/model/user.dart'; +export 'src/wildfire_sink.dart'; +export 'src/controller/user_controller.dart'; +export 'src/controller/identity_controller.dart'; +export 'src/controller/register_controller.dart'; +export 'src/utilities/auth_delegate.dart'; From ee8a7e8aaa23a757226d80361e4c699c424b86df Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Sun, 27 Nov 2016 21:42:54 -0500 Subject: [PATCH 10/36] Cleaning up template project --- .../controller/identity_controller.dart | 2 +- .../controller/register_controller.dart | 2 +- .../{src => }/controller/user_controller.dart | 2 +- .../default/lib/{src => }/model/token.dart | 2 +- .../default/lib/{src => }/model/user.dart | 2 +- .../{src => }/utilities/auth_delegate.dart | 2 +- example/templates/default/lib/wildfire.dart | 14 ++++--- .../default/lib/{src => }/wildfire_sink.dart | 40 ++++++++++++++---- .../{mock/startup.dart => harness/app.dart} | 42 +++++++++++++++++-- .../test/identity_controller_test.dart | 6 +-- .../templates/default/test/register_test.dart | 4 +- .../default/test/user_controller_test.dart | 6 +-- 12 files changed, 86 insertions(+), 38 deletions(-) rename example/templates/default/lib/{src => }/controller/identity_controller.dart (91%) rename example/templates/default/lib/{src => }/controller/register_controller.dart (96%) rename example/templates/default/lib/{src => }/controller/user_controller.dart (95%) rename example/templates/default/lib/{src => }/model/token.dart (98%) rename example/templates/default/lib/{src => }/model/user.dart (94%) rename example/templates/default/lib/{src => }/utilities/auth_delegate.dart (99%) rename example/templates/default/lib/{src => }/wildfire_sink.dart (71%) rename example/templates/default/test/{mock/startup.dart => harness/app.dart} (50%) diff --git a/example/templates/default/lib/src/controller/identity_controller.dart b/example/templates/default/lib/controller/identity_controller.dart similarity index 91% rename from example/templates/default/lib/src/controller/identity_controller.dart rename to example/templates/default/lib/controller/identity_controller.dart index 1eb068315..a14bc386d 100644 --- a/example/templates/default/lib/src/controller/identity_controller.dart +++ b/example/templates/default/lib/controller/identity_controller.dart @@ -1,4 +1,4 @@ -import '../../wildfire.dart'; +import '../wildfire.dart'; class IdentityController extends HTTPController { @httpGet diff --git a/example/templates/default/lib/src/controller/register_controller.dart b/example/templates/default/lib/controller/register_controller.dart similarity index 96% rename from example/templates/default/lib/src/controller/register_controller.dart rename to example/templates/default/lib/controller/register_controller.dart index 7956e2437..e73834726 100644 --- a/example/templates/default/lib/src/controller/register_controller.dart +++ b/example/templates/default/lib/controller/register_controller.dart @@ -1,4 +1,4 @@ -import '../../wildfire.dart'; +import '../wildfire.dart'; class RegisterController extends QueryController { @httpPost diff --git a/example/templates/default/lib/src/controller/user_controller.dart b/example/templates/default/lib/controller/user_controller.dart similarity index 95% rename from example/templates/default/lib/src/controller/user_controller.dart rename to example/templates/default/lib/controller/user_controller.dart index f7f523249..cce562287 100644 --- a/example/templates/default/lib/src/controller/user_controller.dart +++ b/example/templates/default/lib/controller/user_controller.dart @@ -1,4 +1,4 @@ -import '../../wildfire.dart'; +import '../wildfire.dart'; class UserController extends QueryController { @httpGet diff --git a/example/templates/default/lib/src/model/token.dart b/example/templates/default/lib/model/token.dart similarity index 98% rename from example/templates/default/lib/src/model/token.dart rename to example/templates/default/lib/model/token.dart index 56f174df4..d05af2362 100644 --- a/example/templates/default/lib/src/model/token.dart +++ b/example/templates/default/lib/model/token.dart @@ -1,4 +1,4 @@ -import '../../wildfire.dart'; +import '../wildfire.dart'; class AuthCode extends ManagedObject<_AuthCode> implements _AuthCode {} diff --git a/example/templates/default/lib/src/model/user.dart b/example/templates/default/lib/model/user.dart similarity index 94% rename from example/templates/default/lib/src/model/user.dart rename to example/templates/default/lib/model/user.dart index 9700edc00..16d73db84 100644 --- a/example/templates/default/lib/src/model/user.dart +++ b/example/templates/default/lib/model/user.dart @@ -1,4 +1,4 @@ -import '../../wildfire.dart'; +import '../wildfire.dart'; class User extends ManagedObject<_User> implements _User, Authenticatable { @managedTransientInputAttribute diff --git a/example/templates/default/lib/src/utilities/auth_delegate.dart b/example/templates/default/lib/utilities/auth_delegate.dart similarity index 99% rename from example/templates/default/lib/src/utilities/auth_delegate.dart rename to example/templates/default/lib/utilities/auth_delegate.dart index 0bc378fdc..c7b000b4d 100644 --- a/example/templates/default/lib/src/utilities/auth_delegate.dart +++ b/example/templates/default/lib/utilities/auth_delegate.dart @@ -1,4 +1,4 @@ -import '../../wildfire.dart'; +import '../wildfire.dart'; class WildfireAuthenticationDelegate implements AuthServerDelegate { diff --git a/example/templates/default/lib/wildfire.dart b/example/templates/default/lib/wildfire.dart index 2c7932c80..3b81a9fb7 100644 --- a/example/templates/default/lib/wildfire.dart +++ b/example/templates/default/lib/wildfire.dart @@ -12,9 +12,11 @@ export 'dart:io'; export 'package:aqueduct/aqueduct.dart'; export 'package:scribe/scribe.dart'; -export 'src/model/token.dart'; -export 'src/model/user.dart'; -export 'src/controller/user_controller.dart'; -export 'src/controller/identity_controller.dart'; -export 'src/controller/register_controller.dart'; -export 'src/utilities/auth_delegate.dart'; +export 'wildfire_sink.dart'; +export 'model/token.dart'; +export 'model/user.dart'; +export 'controller/user_controller.dart'; +export 'controller/identity_controller.dart'; +export 'controller/register_controller.dart'; +export 'utilities/auth_delegate.dart'; + diff --git a/example/templates/default/lib/src/wildfire_sink.dart b/example/templates/default/lib/wildfire_sink.dart similarity index 71% rename from example/templates/default/lib/src/wildfire_sink.dart rename to example/templates/default/lib/wildfire_sink.dart index fa89735f8..176774a6d 100644 --- a/example/templates/default/lib/src/wildfire_sink.dart +++ b/example/templates/default/lib/wildfire_sink.dart @@ -1,16 +1,24 @@ -import '../wildfire.dart'; - -class WildfireConfiguration extends ConfigurationItem { - WildfireConfiguration(String fileName) : super.fromFile(fileName); - - DatabaseConnectionConfiguration database; - int port; -} - +import 'wildfire.dart'; + +/// This class handles setting up this application. +/// +/// Override methods from [RequestSink] to set up the resources your +/// application uses and the routes it exposes. +/// +/// Instances of this class are the type argument to [Application]. +/// See http://stablekernel.github.io/aqueduct/http/request_sink.html +/// for more details. +/// +/// See bin/start.dart for usage. class WildfireSink extends RequestSink { static const String ConfigurationKey = "ConfigurationKey"; static const String LoggingTargetKey = "LoggingTargetKey"; + /// [Application] creates instances of this type with this constructor. + /// + /// The options will be the values set in the spawning [Application]'s + /// [Application.configuration] [ApplicationConfiguration.configurationOptions]. + /// See bin/start.dart. WildfireSink(Map opts) : super(opts) { configuration = opts[ConfigurationKey]; @@ -27,6 +35,7 @@ class WildfireSink extends RequestSink { AuthServer authenticationServer; WildfireConfiguration configuration; + /// All routes must be configured in this method. @override void setupRouter(Router router) { router @@ -82,3 +91,16 @@ class WildfireSink extends RequestSink { return authenticationServer.documentSecuritySchemes(resolver); } } + +/// An instance of this class represents values from a configuration +/// file specific to this application. +/// +/// Configuration files must have key-value for the properties in this class. +/// For more documentation on configuration files, see +/// https://pub.dartlang.org/packages/safe_config. +class WildfireConfiguration extends ConfigurationItem { + WildfireConfiguration(String fileName) : super.fromFile(fileName); + + DatabaseConnectionConfiguration database; + int port; +} \ No newline at end of file diff --git a/example/templates/default/test/mock/startup.dart b/example/templates/default/test/harness/app.dart similarity index 50% rename from example/templates/default/test/mock/startup.dart rename to example/templates/default/test/harness/app.dart index a9b17e0d0..79acf74a2 100644 --- a/example/templates/default/test/mock/startup.dart +++ b/example/templates/default/test/harness/app.dart @@ -1,8 +1,18 @@ import 'package:wildfire/wildfire.dart'; -import 'package:scribe/scribe.dart'; -import 'dart:async'; +export 'package:wildfire/wildfire.dart'; +export 'package:test/test.dart'; +/// A testing harness for wildfire. +/// +/// Use instances of this class to start/stop the test wildfire server. Use [client] to execute +/// requests against the test server. This instance will create a temporary version of your +/// code's current database schema during startup. This instance will use configuration values +/// from config.yaml.src. class TestApplication { + + /// Creates an instance of this class. + /// + /// Reads configuration values from config.yaml.src. See [start] for usage. TestApplication() { configuration = new WildfireConfiguration("config.yaml.src"); configuration.database.isTemporary = true; @@ -14,6 +24,19 @@ class TestApplication { TestClient client; WildfireConfiguration configuration; + /// Starts running this test harness. + /// + /// This method will start a [LoggingServer] and an [Application] with [WildfireSink]. + /// It will also setup a temporary database connection to the database described in + /// config.yaml.src. The current declared [ManagedObject]s in this application will be + /// used to generate a temporary database schema. The [WildfireSink] instance will use + /// this temporary database. Stopping this application will remove the data from the + /// temporary database. + /// + /// An initial client ID/secret pair will be generated and added to the database + /// for the [client] to use. This value is "com.aqueduct.test"/"kilimanjaro". + /// + /// You must call [stop] on this instance when tearing down your tests. Future start() async { await logger.start(); @@ -35,13 +58,21 @@ class TestApplication { ..clientSecret = "kilimanjaro"; } + /// Stops running this application harness. + /// + /// This method must be called during test tearDown. Future stop() async { await sink.context.persistentStore?.close(); await logger?.stop(); await application?.stop(); } - static Future addClientRecord( + /// Adds a client id/secret pair to the temporary database. + /// + /// [start] must have already been called prior to executing this method. By default, + /// every application harness inserts a default client record during [start]. See [start] + /// for more details. + static Future addClientRecord( {String clientID: "com.aqueduct.test", String clientSecret: "kilimanjaro"}) async { var salt = AuthServer.generateRandomSalt(); @@ -55,9 +86,12 @@ class TestApplication { ..values.id = clientID ..values.salt = salt ..values.hashedPassword = hashedPassword; - await clientQ.insert(); + return await clientQ.insert(); } + /// Adds database tables to the temporary test database based on the declared [ManagedObject]s in this application. + /// + /// This method is executed during [start], and you shouldn't have to invoke it yourself. static Future createDatabaseSchema( ManagedContext context, Logger logger) async { var builder = new SchemaBuilder.toSchema( diff --git a/example/templates/default/test/identity_controller_test.dart b/example/templates/default/test/identity_controller_test.dart index 76b795951..4097290d5 100644 --- a/example/templates/default/test/identity_controller_test.dart +++ b/example/templates/default/test/identity_controller_test.dart @@ -1,8 +1,4 @@ -import 'dart:async'; - -import 'package:test/test.dart'; -import 'package:wildfire/wildfire.dart'; -import 'mock/startup.dart'; +import 'harness/app.dart'; Future main() async { group("Success cases", () { diff --git a/example/templates/default/test/register_test.dart b/example/templates/default/test/register_test.dart index 04060bd58..34f327a05 100644 --- a/example/templates/default/test/register_test.dart +++ b/example/templates/default/test/register_test.dart @@ -1,6 +1,4 @@ -import 'package:test/test.dart'; -import 'package:wildfire/wildfire.dart'; -import 'mock/startup.dart'; +import 'harness/app.dart'; main() { group("Success cases", () { diff --git a/example/templates/default/test/user_controller_test.dart b/example/templates/default/test/user_controller_test.dart index cc99c8fc3..670526a86 100644 --- a/example/templates/default/test/user_controller_test.dart +++ b/example/templates/default/test/user_controller_test.dart @@ -1,9 +1,5 @@ -import 'dart:async'; - -import 'package:test/test.dart'; -import 'package:wildfire/wildfire.dart'; +import 'harness/app.dart'; import 'dart:convert'; -import 'mock/startup.dart'; Future main() async { group("Success cases", () { From 461f99c7a98bdc7283cb4c0d286fe6f7f2195d02 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Sun, 27 Nov 2016 22:58:45 -0500 Subject: [PATCH 11/36] Cleaned up API a bit, hid some things --- example/templates/default/bin/start.dart | 3 - lib/src/application/application.dart | 30 +++--- ...te_server.dart => application_server.dart} | 83 +++++++---------- .../isolate_application_server.dart | 67 +++++++++++++ lib/src/application/isolate_supervisor.dart | 5 +- lib/src/auth/authentication_server.dart | 2 +- lib/src/db/managed/managed.dart | 1 - lib/src/db/managed/object.dart | 22 +++++ lib/src/http/request_controller.dart | 14 ++- lib/src/http/request_path.dart | 93 +------------------ lib/src/http/route_node.dart | 87 +++++++++++++++++ lib/src/http/router.dart | 4 +- lib/src/utilities/token_generator.dart | 4 - test/base/isolate_application_test.dart | 8 +- test/base/pattern_test.dart | 2 + test/base/pipeline_test.dart | 6 +- 16 files changed, 253 insertions(+), 178 deletions(-) rename lib/src/application/{isolate_server.dart => application_server.dart} (52%) create mode 100644 lib/src/application/isolate_application_server.dart diff --git a/example/templates/default/bin/start.dart b/example/templates/default/bin/start.dart index bfee4140b..f3b550d82 100644 --- a/example/templates/default/bin/start.dart +++ b/example/templates/default/bin/start.dart @@ -23,9 +23,6 @@ main() async { var signalPath = new File(".aqueductsignal"); await signalPath.writeAsString("ok"); - } on ApplicationSupervisorException catch (e, st) { - await writeError( - "IsolateSupervisorException, server failed to start: ${e.message} $st"); } catch (e, st) { await writeError("Server failed to start: $e $st"); } diff --git a/lib/src/application/application.dart b/lib/src/application/application.dart index 35a76512d..3f9bb7fbc 100644 --- a/lib/src/application/application.dart +++ b/lib/src/application/application.dart @@ -7,12 +7,12 @@ import 'package:logging/logging.dart'; import '../http/http.dart'; import 'application_configuration.dart'; -import 'isolate_server.dart'; +import 'application_server.dart'; +import 'isolate_application_server.dart'; import 'isolate_supervisor.dart'; export 'application_configuration.dart'; -export 'isolate_server.dart'; -export 'isolate_supervisor.dart'; +export 'application_server.dart'; /// A container for web server applications. /// @@ -23,14 +23,17 @@ class Application { /// Used internally. List supervisors = []; - /// Used internally. + /// The [ApplicationServer] managing delivering HTTP requests into this application. + /// + /// This property is only valid if this application is started with runOnMainIsolate set to true in [start]. + /// Tests may access this property to examine or directly use resources of a [RequestSink]. ApplicationServer server; /// The [RequestSink] receiving requests on the main isolate. /// /// Applications run during testing are run on the main isolate. When running in this way, /// an application will only have one [RequestSinkType] receiving HTTP requests. This property is that instance. - /// If an application is running across multiple isolates, this property will be null. See [start] for more details. + /// If an application is running across multiple isolates, this property is null. See [start] for more details. RequestSinkType get mainIsolateSink => server?.sink as RequestSinkType; /// A reference to a logger. @@ -148,14 +151,13 @@ class Application { } } -/// Used internally. -class ApplicationInitialServerMessage { - String streamTypeName; - Uri streamLibraryURI; - ApplicationConfiguration configuration; - SendPort parentMessagePort; - int identifier; +/// Thrown when an application encounters an exception during startup. +/// +/// Contains the original exception that halted startup. +class ApplicationStartupException implements Exception { + ApplicationStartupException(this.originalException); + + dynamic originalException; - ApplicationInitialServerMessage(this.streamTypeName, this.streamLibraryURI, - this.configuration, this.identifier, this.parentMessagePort); + String toString() => originalException.toString(); } diff --git a/lib/src/application/isolate_server.dart b/lib/src/application/application_server.dart similarity index 52% rename from lib/src/application/isolate_server.dart rename to lib/src/application/application_server.dart index 847f161b8..33be55351 100644 --- a/lib/src/application/isolate_server.dart +++ b/lib/src/application/application_server.dart @@ -1,7 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'dart:isolate'; -import 'dart:mirrors'; import 'package:logging/logging.dart'; @@ -9,20 +7,42 @@ import '../http/request.dart'; import '../http/request_sink.dart'; import 'application.dart'; import 'application_configuration.dart'; -import 'isolate_supervisor.dart'; -/// Used internally. +/// Represents a [RequestSink] manager being used by an [Application]. +/// +/// An Aqueduct application creates instances of this type to pair an HTTP server and an +/// instance of an application-specific [RequestSink]. class ApplicationServer { + + /// The configuration this instance used to start its [sink]. ApplicationConfiguration configuration; + + /// The underlying [HttpServer]. HttpServer server; + + /// The instance of [RequestSink] serving requests. RequestSink sink; + + /// The unique identifier of this instance. + /// + /// Each instance has its own identifier, a numeric value starting at 1, to identify it + /// among other instances. int identifier; + + /// The logger of this instance Logger get logger => new Logger("aqueduct"); + /// Creates an instance of this type. + /// + /// You should not need to invoke this method directly. ApplicationServer(this.sink, this.configuration, this.identifier) { sink.server = this; } + /// Starts this instance, allowing it to receive HTTP requests. + /// + /// Do not invoke this method directly, [Application] instances are responsible + /// for calling this method. Future start({bool shareHttpServer: false}) async { try { sink.setupRouter(sink.router); @@ -49,6 +69,13 @@ class ApplicationServer { } } + /// Invoked when this server becomes ready receive requests. + /// + /// This method will invoke [RequestSink.open] and await for it to finish. + /// Once [RequestSink.open] completes, the underlying [server]'s HTTP requests + /// will be sent to this instance's [sink]. + /// + /// [RequestSink.didOpen] is invoked after this opening has completed. Future didOpen() async { logger.info("Server aqueduct/$identifier started."); @@ -65,51 +92,3 @@ class ApplicationServer { sink.didOpen(); } } - -/// Used internally. -class ApplicationIsolateServer extends ApplicationServer { - SendPort supervisingApplicationPort; - ReceivePort supervisingReceivePort; - - ApplicationIsolateServer( - RequestSink sink, - ApplicationConfiguration configuration, - int identifier, - this.supervisingApplicationPort) - : super(sink, configuration, identifier) { - sink.server = this; - supervisingReceivePort = new ReceivePort(); - supervisingReceivePort.listen(listener); - } - - @override - Future didOpen() async { - await super.didOpen(); - - supervisingApplicationPort.send(supervisingReceivePort.sendPort); - } - - void listener(dynamic message) { - if (message == ApplicationIsolateSupervisor.MessageStop) { - server.close(force: true).then((s) { - supervisingApplicationPort - .send(ApplicationIsolateSupervisor.MessageStop); - }); - } - } -} - -/// This method is used internally. -void isolateServerEntryPoint(ApplicationInitialServerMessage params) { - var sinkSourceLibraryMirror = - currentMirrorSystem().libraries[params.streamLibraryURI]; - var sinkTypeMirror = sinkSourceLibraryMirror.declarations[ - new Symbol(params.streamTypeName)] as ClassMirror; - - var app = sinkTypeMirror.newInstance( - new Symbol(""), [params.configuration.configurationOptions]).reflectee; - - var server = new ApplicationIsolateServer( - app, params.configuration, params.identifier, params.parentMessagePort); - server.start(shareHttpServer: true); -} diff --git a/lib/src/application/isolate_application_server.dart b/lib/src/application/isolate_application_server.dart new file mode 100644 index 000000000..1a05ca2b4 --- /dev/null +++ b/lib/src/application/isolate_application_server.dart @@ -0,0 +1,67 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:mirrors'; + +import '../http/request_sink.dart'; +import 'application.dart'; +import 'application_configuration.dart'; +import 'isolate_supervisor.dart'; + +class ApplicationIsolateServer extends ApplicationServer { + SendPort supervisingApplicationPort; + ReceivePort supervisingReceivePort; + + ApplicationIsolateServer( + RequestSink sink, + ApplicationConfiguration configuration, + int identifier, + this.supervisingApplicationPort) + : super(sink, configuration, identifier) { + sink.server = this; + supervisingReceivePort = new ReceivePort(); + supervisingReceivePort.listen(listener); + } + + @override + Future didOpen() async { + await super.didOpen(); + + supervisingApplicationPort.send(supervisingReceivePort.sendPort); + } + + void listener(dynamic message) { + if (message == ApplicationIsolateSupervisor.MessageStop) { + server.close(force: true).then((s) { + supervisingApplicationPort + .send(ApplicationIsolateSupervisor.MessageStop); + }); + } + } +} + +/// This method is used internally. +void isolateServerEntryPoint(ApplicationInitialServerMessage params) { + var sinkSourceLibraryMirror = + currentMirrorSystem().libraries[params.streamLibraryURI]; + var sinkTypeMirror = sinkSourceLibraryMirror.declarations[ + new Symbol(params.streamTypeName)] as ClassMirror; + + var app = sinkTypeMirror.newInstance( + new Symbol(""), [params.configuration.configurationOptions]).reflectee; + + var server = new ApplicationIsolateServer( + app, params.configuration, params.identifier, params.parentMessagePort); + server.start(shareHttpServer: true); +} + + +class ApplicationInitialServerMessage { + String streamTypeName; + Uri streamLibraryURI; + ApplicationConfiguration configuration; + SendPort parentMessagePort; + int identifier; + + ApplicationInitialServerMessage(this.streamTypeName, this.streamLibraryURI, + this.configuration, this.identifier, this.parentMessagePort); +} diff --git a/lib/src/application/isolate_supervisor.dart b/lib/src/application/isolate_supervisor.dart index 63648ed86..6fc609edc 100644 --- a/lib/src/application/isolate_supervisor.dart +++ b/lib/src/application/isolate_supervisor.dart @@ -64,12 +64,13 @@ class ApplicationIsolateSupervisor { _stopCompleter?.complete(); _stopCompleter = null; } else if (message is List) { - var exception = new ApplicationSupervisorException(message.first); var stacktrace = new StackTrace.fromString(message.last); if (_launchCompleter != null) { - _launchCompleter.completeError(exception, stacktrace); + var appException = new ApplicationStartupException(message.first); + _launchCompleter.completeError(appException, stacktrace); } else { + var exception = new ApplicationSupervisorException(message.first); logger.severe("Uncaught exception in isolate.", exception, stacktrace); } } diff --git a/lib/src/auth/authentication_server.dart b/lib/src/auth/authentication_server.dart index 72ce7e725..b8478a98f 100644 --- a/lib/src/auth/authentication_server.dart +++ b/lib/src/auth/authentication_server.dart @@ -349,7 +349,7 @@ class AuthServer< /// A utility method to generate a random base64 salt. static String generateRandomSalt({int hashLength: 32}) { - var random = new Random(new DateTime.now().millisecondsSinceEpoch); + var random = new Random.secure(); List salt = []; for (var i = 0; i < hashLength; i++) { salt.add(random.nextInt(256)); diff --git a/lib/src/db/managed/managed.dart b/lib/src/db/managed/managed.dart index bd1b3b628..b483bbe80 100644 --- a/lib/src/db/managed/managed.dart +++ b/lib/src/db/managed/managed.dart @@ -1,7 +1,6 @@ export 'attributes.dart'; export 'context.dart'; export 'data_model.dart'; -export 'data_model_builder.dart'; export 'entity.dart'; export 'object.dart'; export 'property_description.dart'; diff --git a/lib/src/db/managed/object.dart b/lib/src/db/managed/object.dart index b360c735d..8de31d644 100644 --- a/lib/src/db/managed/object.dart +++ b/lib/src/db/managed/object.dart @@ -5,14 +5,36 @@ import 'managed.dart'; import 'query_matchable.dart'; import 'backing.dart'; +/// Instances of this class provide storage for [ManagedObject]s. +/// +/// A [ManagedObject] stores properties declared by its type argument in instances of this type. +/// Values are validated against the [ManagedObject.entity]. +/// +/// Instances of this type only store properties for which a value has been explicitly set. This allows +/// serialization classes to omit unset values from the serialized values. Therefore, instances of this class +/// provide behavior that can differentiate between a property being the null value and a property simply not being +/// set. (Therefore, you must use [removeProperty] instead of setting a value to null to really remove it from instances +/// of this type.) +/// +/// Aqueduct implements concrete subclasses of this class to provide behavior for property storage +/// and query-building. abstract class ManagedBacking { + + /// Retrieve a property by its entity and name. dynamic valueForProperty(ManagedEntity entity, String propertyName); + + /// Sets a property by its entity and name. void setValueForProperty( ManagedEntity entity, String propertyName, dynamic value); + + /// Removes a property from this instance. + /// + /// Use this method to use any reference of a property from this instance. void removeProperty(String propertyName) { valueMap.remove(propertyName); } + /// A map of all set values of this instance. Map get valueMap; } diff --git a/lib/src/http/request_controller.dart b/lib/src/http/request_controller.dart index 3a68ad30b..e84b5438e 100644 --- a/lib/src/http/request_controller.dart +++ b/lib/src/http/request_controller.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:logging/logging.dart'; import 'http.dart'; -import '../application/application.dart'; import '../db/db.dart'; /// The unifying protocol for [Request] and [Response] classes. @@ -58,7 +57,7 @@ class RequestController extends Object with APIDocumentable { RequestController pipe(RequestController n) { var typeMirror = reflect(n).type; if (_requestControllerTypeRequiresInstantion(typeMirror)) { - throw new ApplicationSupervisorException( + throw new RequestControllerException( "RequestController subclass ${typeMirror.reflectedType} instances cannot be reused. Rewrite as .generate(() => new ${typeMirror.reflectedType}())"); } this.nextController = n; @@ -328,3 +327,14 @@ class _RequestControllerGenerator extends RequestController { PackagePathResolver resolver) => instantiate().documentSecuritySchemes(resolver); } + +/// Thrown when [RequestController] throws an exception. +/// +/// +class RequestControllerException { + RequestControllerException(this.message); + + String message; + + String toString() => "RequestControllerException: $message"; +} \ No newline at end of file diff --git a/lib/src/http/request_path.dart b/lib/src/http/request_path.dart index d0187afb1..03956dbae 100644 --- a/lib/src/http/request_path.dart +++ b/lib/src/http/request_path.dart @@ -1,4 +1,6 @@ import 'http.dart'; +import 'route_node.dart'; + /// The HTTP request path decomposed into variables and segments based on a [RouteSpecification]. /// @@ -164,97 +166,6 @@ class RouteSpecification extends Object with APIDocumentable { String toString() => segments.join("/"); } -/// Used internally. -class RouteSegment { - RouteSegment(String segment) { - if (segment == "*") { - isRemainingMatcher = true; - return; - } - - var regexIndex = segment.indexOf("("); - if (regexIndex != -1) { - var regexText = segment.substring(regexIndex + 1, segment.length - 1); - matcher = new RegExp(regexText); - - segment = segment.substring(0, regexIndex); - } - - if (segment.startsWith(":")) { - variableName = segment.substring(1, segment.length); - } else if (regexIndex == -1) { - literal = segment; - } - } - - RouteSegment.direct( - {String literal: null, - String variableName: null, - String expression: null, - bool matchesAnything: false}) { - this.literal = literal; - this.variableName = variableName; - this.isRemainingMatcher = matchesAnything; - if (expression != null) { - this.matcher = new RegExp(expression); - } - } - - String literal; - String variableName; - RegExp matcher; - - bool get isLiteralMatcher => - !isRemainingMatcher && !isVariable && !hasRegularExpression; - bool get hasRegularExpression => matcher != null; - bool get isVariable => variableName != null; - bool isRemainingMatcher = false; - - bool matches(String pathSegment) { - if (isLiteralMatcher) { - return pathSegment == literal; - } - - if (hasRegularExpression) { - if (matcher.firstMatch(pathSegment) == null) { - return false; - } - } - - if (isVariable) { - return true; - } - - return false; - } - - operator ==(dynamic other) { - if (other is! RouteSegment) { - return false; - } - - return literal == other.literal && - variableName == other.variableName && - isRemainingMatcher == other.isRemainingMatcher && - matcher?.pattern == other.matcher?.pattern; - } - - String toString() { - if (isLiteralMatcher) { - return literal; - } - - if (isVariable) { - return variableName; - } - - if (hasRegularExpression) { - return "(${matcher.pattern})"; - } - - return "*"; - } -} /// Utility method to take Route syntax into one or more full paths. /// diff --git a/lib/src/http/route_node.dart b/lib/src/http/route_node.dart index 56980d3af..9242e8887 100644 --- a/lib/src/http/route_node.dart +++ b/lib/src/http/route_node.dart @@ -1,5 +1,92 @@ import 'http.dart'; +class RouteSegment { + RouteSegment(String segment) { + if (segment == "*") { + isRemainingMatcher = true; + return; + } + + var regexIndex = segment.indexOf("("); + if (regexIndex != -1) { + var regexText = segment.substring(regexIndex + 1, segment.length - 1); + matcher = new RegExp(regexText); + + segment = segment.substring(0, regexIndex); + } + + if (segment.startsWith(":")) { + variableName = segment.substring(1, segment.length); + } else if (regexIndex == -1) { + literal = segment; + } + } + + RouteSegment.direct( + {String literal: null, + String variableName: null, + String expression: null, + bool matchesAnything: false}) { + this.literal = literal; + this.variableName = variableName; + this.isRemainingMatcher = matchesAnything; + if (expression != null) { + this.matcher = new RegExp(expression); + } + } + + String literal; + String variableName; + RegExp matcher; + + bool get isLiteralMatcher => + !isRemainingMatcher && !isVariable && !hasRegularExpression; + bool get hasRegularExpression => matcher != null; + bool get isVariable => variableName != null; + bool isRemainingMatcher = false; + + bool matches(String pathSegment) { + if (isLiteralMatcher) { + return pathSegment == literal; + } + + if (hasRegularExpression) { + if (matcher.firstMatch(pathSegment) == null) { + return false; + } + } + + if (isVariable) { + return true; + } + + return false; + } + + bool operator ==(dynamic other) { + return literal == other.literal && + variableName == other.variableName && + isRemainingMatcher == other.isRemainingMatcher && + matcher?.pattern == other.matcher?.pattern; + } + + String toString() { + if (isLiteralMatcher) { + return literal; + } + + if (isVariable) { + return variableName; + } + + if (hasRegularExpression) { + return "(${matcher.pattern})"; + } + + return "*"; + } +} + class RouteNode { RouteNode(List specs, {int level: 0, RegExp matcher: null}) { diff --git a/lib/src/http/router.dart b/lib/src/http/router.dart index 5d7d4f03c..c86d62e38 100644 --- a/lib/src/http/router.dart +++ b/lib/src/http/router.dart @@ -172,10 +172,12 @@ class RouteController extends RequestController { final List patterns; } +/// Thrown when a [Router] encounters an exception. class RouterException implements Exception { - final String message; RouterException(this.message); + final String message; + String toString() { return "RouterException: $message"; } diff --git a/lib/src/utilities/token_generator.dart b/lib/src/utilities/token_generator.dart index e2fdd79e9..38bdae54c 100644 --- a/lib/src/utilities/token_generator.dart +++ b/lib/src/utilities/token_generator.dart @@ -1,9 +1,5 @@ import 'dart:math'; -/// A utility to generate a random string of [length]. -/// -/// Will use characters A-Za-z0-9. -/// String randomStringOfLength(int length) { var possibleCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; diff --git a/test/base/isolate_application_test.dart b/test/base/isolate_application_test.dart index 5cdab0662..8bac1a1a7 100644 --- a/test/base/isolate_application_test.dart +++ b/test/base/isolate_application_test.dart @@ -90,7 +90,7 @@ main() { await crashingApp.start(); succeeded = true; } catch (e) { - expect(e.message, "TestException: constructor"); + expect(e.toString(), contains("TestException: constructor")); } expect(succeeded, false); @@ -101,7 +101,7 @@ main() { await crashingApp.start(); succeeded = true; } catch (e) { - expect(e.message, "TestException: addRoutes"); + expect(e.toString(), contains("TestException: addRoutes")); } expect(succeeded, false); @@ -112,7 +112,7 @@ main() { await crashingApp.start(); succeeded = true; } catch (e) { - expect(e.message, "TestException: willOpen"); + expect(e.toString(), contains("TestException: willOpen")); } expect(succeeded, false); @@ -137,7 +137,7 @@ main() { await conflictingApp.start(); successful = true; } catch (e) { - expect(e, new isInstanceOf()); + expect(e, new isInstanceOf()); } expect(successful, false); diff --git a/test/base/pattern_test.dart b/test/base/pattern_test.dart index adc85cb55..41640c7a6 100644 --- a/test/base/pattern_test.dart +++ b/test/base/pattern_test.dart @@ -2,6 +2,8 @@ import "package:test/test.dart"; import "dart:core"; import 'package:aqueduct/aqueduct.dart'; +import '../../lib/src/http/route_node.dart'; + void main() { group("Pattern splitting", () { test("No optionals, no expressions", () { diff --git a/test/base/pipeline_test.dart b/test/base/pipeline_test.dart index 2f88b5ea7..ef9cb1a77 100644 --- a/test/base/pipeline_test.dart +++ b/test/base/pipeline_test.dart @@ -9,9 +9,9 @@ void main() { try { await app.start(); expect(true, false); - } on ApplicationSupervisorException catch (e) { - expect(e.message, - "RequestController subclass FailingController instances cannot be reused. Rewrite as .generate(() => new FailingController())"); + } on ApplicationStartupException catch (e) { + expect(e.toString(), + contains("RequestController subclass FailingController instances cannot be reused. Rewrite as .generate(() => new FailingController())")); } }); } From 2c3966a13019120754f11c8872940eb5237cd2a0 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 28 Nov 2016 11:25:13 -0500 Subject: [PATCH 12/36] Moved isolate exception back to public to prevent from breaking existing code, marked as deprecated --- lib/src/application/application.dart | 12 ++++++++++++ lib/src/application/isolate_supervisor.dart | 11 ----------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/src/application/application.dart b/lib/src/application/application.dart index 3f9bb7fbc..6cf9e0dbf 100644 --- a/lib/src/application/application.dart +++ b/lib/src/application/application.dart @@ -161,3 +161,15 @@ class ApplicationStartupException implements Exception { String toString() => originalException.toString(); } + +/// An exception originating from an [Isolate] within an [Application]. +@Deprecated("This class will become private in 1.1. Use ApplicationStartupException in its place.") +class ApplicationSupervisorException implements Exception { + ApplicationSupervisorException(this.message); + + final String message; + + String toString() { + return "$message"; + } +} diff --git a/lib/src/application/isolate_supervisor.dart b/lib/src/application/isolate_supervisor.dart index 6fc609edc..27841ef05 100644 --- a/lib/src/application/isolate_supervisor.dart +++ b/lib/src/application/isolate_supervisor.dart @@ -89,14 +89,3 @@ class ApplicationIsolateSupervisor { }); } } - -/// An exception originating from an [Isolate] within an [Application]. -class ApplicationSupervisorException implements Exception { - ApplicationSupervisorException(this.message); - - final String message; - - String toString() { - return "$message"; - } -} From 13bd5e263c7c1d4f16074d75eb5ef2ee556eda35 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 28 Nov 2016 16:28:51 -0500 Subject: [PATCH 13/36] dartfmt --- CHANGELOG.md | 1 + lib/src/application/application.dart | 3 +- lib/src/application/application_server.dart | 1 - .../isolate_application_server.dart | 5 +- lib/src/auth/auth.dart | 2 +- lib/src/auth/authentication_server.dart | 1 - lib/src/auth/client.dart | 2 - lib/src/db/db.dart | 2 +- lib/src/db/managed/context.dart | 4 +- lib/src/db/managed/data_model_builder.dart | 6 +- lib/src/db/managed/entity.dart | 3 +- lib/src/db/managed/object.dart | 1 - lib/src/db/managed/set.dart | 1 - .../postgresql_persistent_store.dart | 15 +- lib/src/db/query/matcher_internal.dart | 1 - lib/src/db/query/query.dart | 20 ++- lib/src/db/query/query_mapping.dart | 123 ++++++------- lib/src/db/query/sort_descriptor.dart | 2 - lib/src/db/schema/migration.dart | 7 +- lib/src/db/schema/schema.dart | 5 +- lib/src/db/schema/schema_builder.dart | 3 +- lib/src/http/controller_routing.dart | 4 +- lib/src/http/http.dart | 2 +- lib/src/http/http_controller.dart | 34 ++-- lib/src/http/http_controller_internal.dart | 12 +- lib/src/http/request_controller.dart | 2 +- lib/src/http/request_path.dart | 2 - lib/src/http/resource_controller.dart | 1 - lib/src/http/route_node.dart | 5 +- lib/src/http/serializable.dart | 2 - lib/src/utilities/pbkdf2.dart | 2 - lib/src/utilities/test_client.dart | 1 - test/base/controller_test.dart | 16 +- test/base/pipeline_test.dart | 6 +- test/base/request_controller_test.dart | 165 +++++++++--------- test/utilities/test_client_test.dart | 21 ++- 36 files changed, 240 insertions(+), 243 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee598377..437c8f420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 1.0.4 - Added new `Response.contentType` property. Adding "Content-Type" to the headers of a `Response` no longer has any effect; use this property instead. +- `ManagedDataModel`s now scan library dependencies for `ManagedObject` subclasses to generate a data model. ## 1.0.3 - Fix to allow Windows user to use `aqueduct setup`. diff --git a/lib/src/application/application.dart b/lib/src/application/application.dart index 6cf9e0dbf..83e93d2d8 100644 --- a/lib/src/application/application.dart +++ b/lib/src/application/application.dart @@ -163,7 +163,8 @@ class ApplicationStartupException implements Exception { } /// An exception originating from an [Isolate] within an [Application]. -@Deprecated("This class will become private in 1.1. Use ApplicationStartupException in its place.") +@Deprecated( + "This class will become private in 1.1. Use ApplicationStartupException in its place.") class ApplicationSupervisorException implements Exception { ApplicationSupervisorException(this.message); diff --git a/lib/src/application/application_server.dart b/lib/src/application/application_server.dart index 33be55351..e41a3d9ef 100644 --- a/lib/src/application/application_server.dart +++ b/lib/src/application/application_server.dart @@ -13,7 +13,6 @@ import 'application_configuration.dart'; /// An Aqueduct application creates instances of this type to pair an HTTP server and an /// instance of an application-specific [RequestSink]. class ApplicationServer { - /// The configuration this instance used to start its [sink]. ApplicationConfiguration configuration; diff --git a/lib/src/application/isolate_application_server.dart b/lib/src/application/isolate_application_server.dart index 1a05ca2b4..967de3b6e 100644 --- a/lib/src/application/isolate_application_server.dart +++ b/lib/src/application/isolate_application_server.dart @@ -42,9 +42,9 @@ class ApplicationIsolateServer extends ApplicationServer { /// This method is used internally. void isolateServerEntryPoint(ApplicationInitialServerMessage params) { var sinkSourceLibraryMirror = - currentMirrorSystem().libraries[params.streamLibraryURI]; + currentMirrorSystem().libraries[params.streamLibraryURI]; var sinkTypeMirror = sinkSourceLibraryMirror.declarations[ - new Symbol(params.streamTypeName)] as ClassMirror; + new Symbol(params.streamTypeName)] as ClassMirror; var app = sinkTypeMirror.newInstance( new Symbol(""), [params.configuration.configurationOptions]).reflectee; @@ -54,7 +54,6 @@ void isolateServerEntryPoint(ApplicationInitialServerMessage params) { server.start(shareHttpServer: true); } - class ApplicationInitialServerMessage { String streamTypeName; Uri streamLibraryURI; diff --git a/lib/src/auth/auth.dart b/lib/src/auth/auth.dart index d128abe76..d7b082cd0 100644 --- a/lib/src/auth/auth.dart +++ b/lib/src/auth/auth.dart @@ -4,4 +4,4 @@ export 'authentication_server.dart'; export 'authorization_parser.dart'; export 'authorizer.dart'; export 'client.dart'; -export 'protocols.dart'; \ No newline at end of file +export 'protocols.dart'; diff --git a/lib/src/auth/authentication_server.dart b/lib/src/auth/authentication_server.dart index b8478a98f..db95f6743 100644 --- a/lib/src/auth/authentication_server.dart +++ b/lib/src/auth/authentication_server.dart @@ -11,7 +11,6 @@ import '../utilities/pbkdf2.dart'; import '../utilities/token_generator.dart'; import 'auth.dart'; - /// A storage-agnostic authorization 'server'. /// /// Instances of this type will carry out authentication and authorization tasks. This class shouldn't be subclassed. The storage required by tasks performed diff --git a/lib/src/auth/client.dart b/lib/src/auth/client.dart index 64aff14bd..10e4a7e51 100644 --- a/lib/src/auth/client.dart +++ b/lib/src/auth/client.dart @@ -1,5 +1,3 @@ - - /// Represents a Client ID and secret pair. class AuthClient { /// Creates an instance of [AuthClient]. diff --git a/lib/src/db/db.dart b/lib/src/db/db.dart index 55b52f791..0a00c4f58 100644 --- a/lib/src/db/db.dart +++ b/lib/src/db/db.dart @@ -2,4 +2,4 @@ export 'managed/managed.dart'; export 'persistent_store/persistent_store.dart'; export 'postgresql/postgresql_persistent_store.dart'; export 'query/query.dart'; -export 'schema/schema.dart'; \ No newline at end of file +export 'schema/schema.dart'; diff --git a/lib/src/db/managed/context.dart b/lib/src/db/managed/context.dart index 9ea99f46c..9cc3fc794 100644 --- a/lib/src/db/managed/context.dart +++ b/lib/src/db/managed/context.dart @@ -71,8 +71,8 @@ class ManagedContext { } Future executeDeleteQuery(Query query) async { - return await persistentStore.executeDeleteQuery( - query.persistentQueryForStore(persistentStore)); + return await persistentStore + .executeDeleteQuery(query.persistentQueryForStore(persistentStore)); } List _coalesceAndMapRows( diff --git a/lib/src/db/managed/data_model_builder.dart b/lib/src/db/managed/data_model_builder.dart index 1f5f41b60..01daae49a 100644 --- a/lib/src/db/managed/data_model_builder.dart +++ b/lib/src/db/managed/data_model_builder.dart @@ -7,8 +7,10 @@ class DataModelBuilder { instanceTypes.forEach((type) { var backingMirror = backingMirrorForType(type); var entity = new ManagedEntity( - dataModel, tableNameForPersistentTypeMirror(backingMirror), - reflectClass(type), backingMirror); + dataModel, + tableNameForPersistentTypeMirror(backingMirror), + reflectClass(type), + backingMirror); entities[type] = entity; persistentTypeToEntityMap[entity.persistentType.reflectedType] = entity; diff --git a/lib/src/db/managed/entity.dart b/lib/src/db/managed/entity.dart index ea3c105bb..d5fb43a7b 100644 --- a/lib/src/db/managed/entity.dart +++ b/lib/src/db/managed/entity.dart @@ -26,7 +26,8 @@ class ManagedEntity { /// Creates an instance of this type.. /// /// You should never call this method directly, it will be called by [ManagedDataModel]. - ManagedEntity(this.dataModel, this._tableName, this.instanceType, this.persistentType); + ManagedEntity( + this.dataModel, this._tableName, this.instanceType, this.persistentType); /// The type of instances represented by this entity. /// diff --git a/lib/src/db/managed/object.dart b/lib/src/db/managed/object.dart index 8de31d644..b82e6d65b 100644 --- a/lib/src/db/managed/object.dart +++ b/lib/src/db/managed/object.dart @@ -19,7 +19,6 @@ import 'backing.dart'; /// Aqueduct implements concrete subclasses of this class to provide behavior for property storage /// and query-building. abstract class ManagedBacking { - /// Retrieve a property by its entity and name. dynamic valueForProperty(ManagedEntity entity, String propertyName); diff --git a/lib/src/db/managed/set.dart b/lib/src/db/managed/set.dart index 0869106f3..657ff9054 100644 --- a/lib/src/db/managed/set.dart +++ b/lib/src/db/managed/set.dart @@ -5,7 +5,6 @@ import '../../http/serializable.dart'; import 'managed.dart'; import 'query_matchable.dart'; - /// Instances of this type contain zero or more instances of [ManagedObject]. /// /// 'Has many' relationship properties in [ManagedObject]s are represented by this type. [ManagedSet]s properties may only be declared in the persistent diff --git a/lib/src/db/postgresql/postgresql_persistent_store.dart b/lib/src/db/postgresql/postgresql_persistent_store.dart index 3f9988c69..fad6fb35a 100644 --- a/lib/src/db/postgresql/postgresql_persistent_store.dart +++ b/lib/src/db/postgresql/postgresql_persistent_store.dart @@ -209,8 +209,8 @@ class PostgreSQLPersistentStore extends PersistentStore value: (PersistentColumnMapping m) => m.value); var queryStringBuffer = new StringBuffer(); - queryStringBuffer.write( - "INSERT INTO ${q.entity.tableName} ($columnsBeingInserted) "); + queryStringBuffer + .write("INSERT INTO ${q.entity.tableName} ($columnsBeingInserted) "); queryStringBuffer.write("VALUES (${valueKeysToBeInserted}) "); if (q.resultKeys != null && q.resultKeys.length > 0) { @@ -245,8 +245,8 @@ class PostgreSQLPersistentStore extends PersistentStore } }).join(","); - var queryStringBuffer = new StringBuffer( - "SELECT $columnsToFetch FROM ${q.entity.tableName} "); + var queryStringBuffer = + new StringBuffer("SELECT $columnsToFetch FROM ${q.entity.tableName} "); joinElements.forEach((PersistentColumnMapping je) { PersistentJoinMapping joinElement = je; queryStringBuffer.write("${_joinStringForJoin(joinElement)} "); @@ -328,8 +328,7 @@ class PostgreSQLPersistentStore extends PersistentStore }).join(","); var queryStringBuffer = new StringBuffer(); - queryStringBuffer - .write("UPDATE ${q.entity.tableName} SET $setPairString "); + queryStringBuffer.write("UPDATE ${q.entity.tableName} SET $setPairString "); if (q.predicate != null) { queryStringBuffer.write("where ${q.predicate.format} "); @@ -563,8 +562,8 @@ class PostgreSQLPersistentStore extends PersistentStore var operator = (query.pageDescriptor.order == QuerySortOrder.ascending ? ">" : "<"); var keyName = "aq_page_value"; - var typedKeyName = _typedColumnName(keyName, - query.entity.properties[query.pageDescriptor.propertyName]); + var typedKeyName = _typedColumnName( + keyName, query.entity.properties[query.pageDescriptor.propertyName]); return new QueryPredicate( "${query.pageDescriptor.propertyName} ${operator} @$typedKeyName", {"$keyName": query.pageDescriptor.boundingValue}); diff --git a/lib/src/db/query/matcher_internal.dart b/lib/src/db/query/matcher_internal.dart index af07e7ed7..b025bb298 100644 --- a/lib/src/db/query/matcher_internal.dart +++ b/lib/src/db/query/matcher_internal.dart @@ -34,4 +34,3 @@ class StringMatcherExpression implements MatcherExpression { StringMatcherOperator operator; String value; } - diff --git a/lib/src/db/query/query.dart b/lib/src/db/query/query.dart index 18a43d4ad..3029b5920 100644 --- a/lib/src/db/query/query.dart +++ b/lib/src/db/query/query.dart @@ -141,11 +141,13 @@ class Query { PersistentStoreQuery persistentQueryForStore(PersistentStore store) { var psq = new PersistentStoreQuery() - ..confirmQueryModifiesAllInstancesOnDeleteOrUpdate = confirmQueryModifiesAllInstancesOnDeleteOrUpdate - ..entity = entity - ..timeoutInSeconds = timeoutInSeconds - ..sortDescriptors = sortDescriptors - ..resultKeys = mappingElementsForList((resultProperties ?? entity.defaultProperties), entity); + ..confirmQueryModifiesAllInstancesOnDeleteOrUpdate = + confirmQueryModifiesAllInstancesOnDeleteOrUpdate + ..entity = entity + ..timeoutInSeconds = timeoutInSeconds + ..sortDescriptors = sortDescriptors + ..resultKeys = mappingElementsForList( + (resultProperties ?? entity.defaultProperties), entity); if (_matchOn != null) { psq.predicate = new QueryPredicate.fromQueryIncludable(_matchOn, store); @@ -157,7 +159,7 @@ class Query { if (pageDescriptor != null) { throw new QueryException(QueryExceptionEvent.requestFailure, message: - "Query cannot have properties that are includeInResultSet and also have a pageDescriptor."); + "Query cannot have properties that are includeInResultSet and also have a pageDescriptor."); } var joinElements = joinElementsFromQueryMatchable( @@ -169,8 +171,8 @@ class Query { psq.pageDescriptor = validatePageDescriptor(entity, pageDescriptor); - psq.values = mappingElementsForMap( - (valueMap ?? values?.backingMap), entity); + psq.values = + mappingElementsForMap((valueMap ?? values?.backingMap), entity); } return psq; @@ -332,4 +334,4 @@ abstract class QueryMatchable { bool includeInResultSet; Map get backingMap; -} \ No newline at end of file +} diff --git a/lib/src/db/query/query_mapping.dart b/lib/src/db/query/query_mapping.dart index 91bc40ec5..4503be2c2 100644 --- a/lib/src/db/query/query_mapping.dart +++ b/lib/src/db/query/query_mapping.dart @@ -6,25 +6,27 @@ import '../persistent_store/persistent_store.dart'; import '../managed/query_matchable.dart'; import '../persistent_store/persistent_store_query.dart'; -ManagedPropertyDescription _propertyForName(ManagedEntity entity, String propertyName) { +ManagedPropertyDescription _propertyForName( + ManagedEntity entity, String propertyName) { var property = entity.properties[propertyName]; if (property == null) { throw new QueryException(QueryExceptionEvent.internalFailure, message: - "Property $propertyName does not exist on ${entity.tableName}"); + "Property $propertyName does not exist on ${entity.tableName}"); } if (property is ManagedRelationshipDescription && property.relationshipType != ManagedRelationshipType.belongsTo) { throw new QueryException(QueryExceptionEvent.internalFailure, message: - "Property $propertyName is a hasMany or hasOne relationship and is invalid as a result property of ${entity + "Property $propertyName is a hasMany or hasOne relationship and is invalid as a result property of ${entity .tableName}, use matchOn.$propertyName.includeInResultSet = true instead."); } return property; } -List mappingElementsForList(List keys, ManagedEntity entity) { +List mappingElementsForList( + List keys, ManagedEntity entity) { if (!keys.contains(entity.primaryKey)) { keys.add(entity.primaryKey); } @@ -44,57 +46,58 @@ QueryPage validatePageDescriptor(ManagedEntity entity, QueryPage page) { if (prop == null) { throw new QueryException(QueryExceptionEvent.requestFailure, message: - "Property ${page.propertyName} in pageDescriptor does not exist on ${entity.tableName}."); + "Property ${page.propertyName} in pageDescriptor does not exist on ${entity.tableName}."); } if (page.boundingValue != null && !prop.isAssignableWith(page.boundingValue)) { throw new QueryException(QueryExceptionEvent.requestFailure, message: - "Property ${page.propertyName} in pageDescriptor has invalid type (${page.boundingValue.runtimeType})."); + "Property ${page.propertyName} in pageDescriptor has invalid type (${page.boundingValue.runtimeType})."); } return page; } -List mappingElementsForMap(Map valueMap, ManagedEntity entity) { +List mappingElementsForMap( + Map valueMap, ManagedEntity entity) { return valueMap?.keys ?.map((key) { - var property = entity.properties[key]; - if (property == null) { - throw new QueryException(QueryExceptionEvent.requestFailure, - message: - "Property $key in values does not exist on ${entity.tableName}"); - } - - var value = valueMap[key]; - if (property is ManagedRelationshipDescription) { - if (property.relationshipType != - ManagedRelationshipType.belongsTo) { - return null; - } - - if (value != null) { - if (value is ManagedObject) { - value = value[property.destinationEntity.primaryKey]; - } else if (value is Map) { - value = value[property.destinationEntity.primaryKey]; - } else { - throw new QueryException(QueryExceptionEvent.internalFailure, + var property = entity.properties[key]; + if (property == null) { + throw new QueryException(QueryExceptionEvent.requestFailure, message: - "Property $key on ${entity.tableName} in Query values must be a Map or ${MirrorSystem.getName( + "Property $key in values does not exist on ${entity.tableName}"); + } + + var value = valueMap[key]; + if (property is ManagedRelationshipDescription) { + if (property.relationshipType != ManagedRelationshipType.belongsTo) { + return null; + } + + if (value != null) { + if (value is ManagedObject) { + value = value[property.destinationEntity.primaryKey]; + } else if (value is Map) { + value = value[property.destinationEntity.primaryKey]; + } else { + throw new QueryException(QueryExceptionEvent.internalFailure, + message: + "Property $key on ${entity.tableName} in Query values must be a Map or ${MirrorSystem.getName( property.destinationEntity.instanceType.simpleName)} "); + } + } } - } - } - return new PersistentColumnMapping(property, value); - }) + return new PersistentColumnMapping(property, value); + }) ?.where((m) => m != null) ?.toList(); } -List joinElementsFromQueryMatchable(QueryMatchableExtension matcherBackedObject, +List joinElementsFromQueryMatchable( + QueryMatchableExtension matcherBackedObject, PersistentStore store, Map> nestedResultProperties) { var entity = matcherBackedObject.entity; @@ -102,31 +105,31 @@ List joinElementsFromQueryMatchable(QueryMatchableExtensi return propertiesToJoin .map((propertyName) { - QueryMatchableExtension inner = - matcherBackedObject.backingMap[propertyName]; - - var relDesc = entity.relationships[propertyName]; - var predicate = new QueryPredicate.fromQueryIncludable(inner, store); - var nestedProperties = - nestedResultProperties[inner.entity.instanceType.reflectedType]; - var propertiesToFetch = - nestedProperties ?? inner.entity.defaultProperties; - - var joinElements = [ - new PersistentJoinMapping( - PersistentJoinType.leftOuter, - relDesc, - predicate, - mappingElementsForList(propertiesToFetch, inner.entity)) - ]; - - if (inner.hasJoinElements) { - joinElements.addAll(joinElementsFromQueryMatchable( - inner, store, nestedResultProperties)); - } - - return joinElements; - }) + QueryMatchableExtension inner = + matcherBackedObject.backingMap[propertyName]; + + var relDesc = entity.relationships[propertyName]; + var predicate = new QueryPredicate.fromQueryIncludable(inner, store); + var nestedProperties = + nestedResultProperties[inner.entity.instanceType.reflectedType]; + var propertiesToFetch = + nestedProperties ?? inner.entity.defaultProperties; + + var joinElements = [ + new PersistentJoinMapping( + PersistentJoinType.leftOuter, + relDesc, + predicate, + mappingElementsForList(propertiesToFetch, inner.entity)) + ]; + + if (inner.hasJoinElements) { + joinElements.addAll(joinElementsFromQueryMatchable( + inner, store, nestedResultProperties)); + } + + return joinElements; + }) .expand((l) => l) .toList(); -} \ No newline at end of file +} diff --git a/lib/src/db/query/sort_descriptor.dart b/lib/src/db/query/sort_descriptor.dart index e29fbdac6..f0d50a06d 100644 --- a/lib/src/db/query/sort_descriptor.dart +++ b/lib/src/db/query/sort_descriptor.dart @@ -1,5 +1,3 @@ - - /// Order value for [QuerySortDescriptor]s and [QueryPage]s. enum QuerySortOrder { /// Ascending order. Example: 1, 2, 3, 4, ... diff --git a/lib/src/db/schema/migration.dart b/lib/src/db/schema/migration.dart index fcff0c3fe..50ef25bdc 100644 --- a/lib/src/db/schema/migration.dart +++ b/lib/src/db/schema/migration.dart @@ -85,10 +85,9 @@ class MigrationExecutor { return m; } catch (e) { throw new MigrationException( - "Migration files must have the following format: Version_Name.migration.dart," - "where Version must be an integer (optionally prefixed with 0s, e.g. '00000002')" - " and '_Name' is optional. Offender: ${fse.uri}" - ); + "Migration files must have the following format: Version_Name.migration.dart," + "where Version must be an integer (optionally prefixed with 0s, e.g. '00000002')" + " and '_Name' is optional. Offender: ${fse.uri}"); } }); diff --git a/lib/src/db/schema/schema.dart b/lib/src/db/schema/schema.dart index bbe874322..348e7dc81 100644 --- a/lib/src/db/schema/schema.dart +++ b/lib/src/db/schema/schema.dart @@ -21,9 +21,8 @@ class Schema { Schema(this.tables); Schema.fromDataModel(ManagedDataModel dataModel) { - tables = dataModel.entities - .map((e) => new SchemaTable.fromEntity(e)) - .toList(); + tables = + dataModel.entities.map((e) => new SchemaTable.fromEntity(e)).toList(); } Schema.from(Schema otherSchema) { diff --git a/lib/src/db/schema/schema_builder.dart b/lib/src/db/schema/schema_builder.dart index 29f4383ce..0375ff13b 100644 --- a/lib/src/db/schema/schema_builder.dart +++ b/lib/src/db/schema/schema_builder.dart @@ -252,8 +252,7 @@ class SchemaBuilder { '${spaceOffset}new SchemaColumn.relationship("${column.name}", ${column.type}'); builder.write(", relatedTableName: \"${column.relatedTableName}\""); builder.write(", relatedColumnName: \"${column.relatedColumnName}\""); - builder.write( - ", rule: ${column.deleteRule}"); + builder.write(", rule: ${column.deleteRule}"); } else { builder.write( '${spaceOffset}new SchemaColumn("${column.name}", ${column.type}'); diff --git a/lib/src/http/controller_routing.dart b/lib/src/http/controller_routing.dart index 4b75943a9..4826b76e8 100644 --- a/lib/src/http/controller_routing.dart +++ b/lib/src/http/controller_routing.dart @@ -42,7 +42,7 @@ class HTTPMethod { /// Marks a controller HTTPHeader or HTTPQuery property as required. const HTTPRequiredParameter requiredHTTPParameter = -const HTTPRequiredParameter(); + const HTTPRequiredParameter(); class HTTPRequiredParameter { const HTTPRequiredParameter(); @@ -65,4 +65,4 @@ class HTTPHeader extends HTTPParameter { /// value is case-sensitive. class HTTPQuery extends HTTPParameter { const HTTPQuery(String key) : super(key); -} \ No newline at end of file +} diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index 6f467a6d8..59d529639 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -12,4 +12,4 @@ export 'request_sink.dart'; export 'resource_controller.dart'; export 'response.dart'; export 'router.dart'; -export 'serializable.dart'; \ No newline at end of file +export 'serializable.dart'; diff --git a/lib/src/http/http_controller.dart b/lib/src/http/http_controller.dart index 6830d875c..677551871 100644 --- a/lib/src/http/http_controller.dart +++ b/lib/src/http/http_controller.dart @@ -27,7 +27,7 @@ import 'http_controller_internal.dart'; @cannotBeReused abstract class HTTPController extends RequestController { static ContentType _applicationWWWFormURLEncodedContentType = - new ContentType("application", "x-www-form-urlencoded"); + new ContentType("application", "x-www-form-urlencoded"); /// The request being processed by this [HTTPController]. /// @@ -88,9 +88,9 @@ abstract class HTTPController extends RequestController { bool _requestContentTypeIsSupported(Request req) { var incomingContentType = request.innerRequest.headers.contentType; return acceptedContentTypes.firstWhere((ct) { - return ct.primaryType == incomingContentType.primaryType && - ct.subType == incomingContentType.subType; - }, orElse: () => null) != + return ct.primaryType == incomingContentType.primaryType && + ct.subType == incomingContentType.subType; + }, orElse: () => null) != null; } @@ -131,7 +131,7 @@ abstract class HTTPController extends RequestController { } var orderedParameters = - mapper.positionalParametersFromRequest(request, queryParameters); + mapper.positionalParametersFromRequest(request, queryParameters); var controllerProperties = controllerCache.propertiesFromRequest( request.innerRequest.headers, queryParameters); var missingParameters = [orderedParameters, controllerProperties.values] @@ -148,10 +148,10 @@ abstract class HTTPController extends RequestController { Future eventualResponse = reflect(this) .invoke( - mapper.methodSymbol, - orderedParameters, - mapper.optionalParametersFromRequest( - request.innerRequest.headers, queryParameters)) + mapper.methodSymbol, + orderedParameters, + mapper.optionalParametersFromRequest( + request.innerRequest.headers, queryParameters)) .reflectee as Future; var response = await eventualResponse; @@ -222,7 +222,7 @@ abstract class HTTPController extends RequestController { var comment = methodDeclaration.documentationComment; var tokens = comment?.tokens ?? []; var lines = - tokens.map((t) => t.lexeme.trimLeft().substring(3).trim()).toList(); + tokens.map((t) => t.lexeme.trimLeft().substring(3).trim()).toList(); if (lines.length > 0) { op.summary = lines.first; } @@ -234,7 +234,7 @@ abstract class HTTPController extends RequestController { bool usesFormEncodedData = op.method.toLowerCase() == "post" && acceptedContentTypes.any((ct) => - ct.primaryType == "application" && + ct.primaryType == "application" && ct.subType == "x-www-form-urlencoded"); op.parameters = [ @@ -242,7 +242,8 @@ abstract class HTTPController extends RequestController { cachedMethod.optionalParameters.values, controllerCache.propertyCache.values ].expand((i) => i.toList()).map((param) { - var paramLocation = _parameterLocationFromHTTPParameter(param.httpParameter); + var paramLocation = + _parameterLocationFromHTTPParameter(param.httpParameter); if (usesFormEncodedData && paramLocation == APIParameterLocation.query) { paramLocation = APIParameterLocation.formData; @@ -253,7 +254,7 @@ abstract class HTTPController extends RequestController { ..required = param.isRequired ..parameterLocation = paramLocation ..schemaObject = - (new APISchemaObject.fromTypeMirror(param.typeMirror)); + (new APISchemaObject.fromTypeMirror(param.typeMirror)); }).toList(); return op; @@ -288,7 +289,8 @@ abstract class HTTPController extends RequestController { } } -Response _missingRequiredParameterResponseIfNecessary(List params) { +Response _missingRequiredParameterResponseIfNecessary( + List params) { var missingHeaders = params .where((p) => p.type == HTTPControllerMissingParameterType.header) .map((p) => p.externalName) @@ -301,7 +303,7 @@ Response _missingRequiredParameterResponseIfNecessary(List "'${p}'").join(", "); + missingQueryParameters.map((p) => "'${p}'").join(", "); missings.write("Missing query value(s): ${missingQueriesString}."); } if (missingQueryParameters.isNotEmpty && missingHeaders.isNotEmpty) { @@ -325,4 +327,4 @@ APIParameterLocation _parameterLocationFromHTTPParameter(HTTPParameter p) { } return null; -} \ No newline at end of file +} diff --git a/lib/src/http/http_controller_internal.dart b/lib/src/http/http_controller_internal.dart index 9ec8ecc53..071b2919f 100644 --- a/lib/src/http/http_controller_internal.dart +++ b/lib/src/http/http_controller_internal.dart @@ -38,7 +38,6 @@ abstract class HTTPParameter { final String externalName; } - class HTTPControllerCache { static Map controllerCache = {}; static HTTPControllerCache cacheForType(Type t) { @@ -57,7 +56,7 @@ class HTTPControllerCache { allDeclarations.values .where((decl) => decl is VariableMirror) .where( - (decl) => decl.metadata.any((im) => im.reflectee is HTTPParameter)) + (decl) => decl.metadata.any((im) => im.reflectee is HTTPParameter)) .forEach((decl) { HTTPControllerCachedParameter param; var isRequired = allDeclarations[decl.simpleName] @@ -163,12 +162,12 @@ class HTTPControllerCachedMethod { req.path.variables[param.name], param.typeMirror); } else if (param.httpParameter is HTTPQuery) { return convertParameterListWithMirror( - queryParameters[param.name], param.typeMirror) ?? + queryParameters[param.name], param.typeMirror) ?? new HTTPControllerMissingParameter( HTTPControllerMissingParameterType.query, param.name); } else if (param.httpParameter is HTTPHeader) { return convertParameterListWithMirror( - req.innerRequest.headers[param.name], param.typeMirror) ?? + req.innerRequest.headers[param.name], param.typeMirror) ?? new HTTPControllerMissingParameter( HTTPControllerMissingParameterType.header, param.name); } @@ -245,7 +244,7 @@ dynamic convertParameterListWithMirror( if (typeMirror.isSubtypeOf(reflectType(List))) { return parameterValues .map((str) => - convertParameterWithMirror(str, typeMirror.typeArguments.first)) + convertParameterWithMirror(str, typeMirror.typeArguments.first)) .toList(); } else { return convertParameterWithMirror(parameterValues.first, typeMirror); @@ -271,7 +270,7 @@ dynamic convertParameterWithMirror( if (parseDecl != null) { try { var reflValue = - typeMirror.invoke(parseDecl.simpleName, [parameterValue]); + typeMirror.invoke(parseDecl.simpleName, [parameterValue]); return reflValue.reflectee; } catch (e) { throw new InternalControllerException( @@ -287,4 +286,3 @@ dynamic convertParameterWithMirror( HttpStatus.INTERNAL_SERVER_ERROR, responseMessage: "URI parameter is wrong type"); } - diff --git a/lib/src/http/request_controller.dart b/lib/src/http/request_controller.dart index e84b5438e..675faa390 100644 --- a/lib/src/http/request_controller.dart +++ b/lib/src/http/request_controller.dart @@ -337,4 +337,4 @@ class RequestControllerException { String message; String toString() => "RequestControllerException: $message"; -} \ No newline at end of file +} diff --git a/lib/src/http/request_path.dart b/lib/src/http/request_path.dart index 03956dbae..97e1b6c7a 100644 --- a/lib/src/http/request_path.dart +++ b/lib/src/http/request_path.dart @@ -1,7 +1,6 @@ import 'http.dart'; import 'route_node.dart'; - /// The HTTP request path decomposed into variables and segments based on a [RouteSpecification]. /// /// After passing through a [Router], a [Request] will have an instance of [HTTPRequestPath] in [Request.path]. @@ -166,7 +165,6 @@ class RouteSpecification extends Object with APIDocumentable { String toString() => segments.join("/"); } - /// Utility method to take Route syntax into one or more full paths. /// /// This method strips away optionals in the route syntax, yielding an individual path for every combination of the route syntax. diff --git a/lib/src/http/resource_controller.dart b/lib/src/http/resource_controller.dart index 3c72a90a9..e577baf8a 100644 --- a/lib/src/http/resource_controller.dart +++ b/lib/src/http/resource_controller.dart @@ -4,7 +4,6 @@ import 'dart:io'; import '../db/db.dart'; import 'http.dart'; - /// A [RequestController] for performing CRUD operations on [ManagedObject] instances. /// /// Instances of this class create and execute [Query]s based on [Request]'s they receive. This instances of this class effectively map a REST API call diff --git a/lib/src/http/route_node.dart b/lib/src/http/route_node.dart index 9242e8887..31d95f4db 100644 --- a/lib/src/http/route_node.dart +++ b/lib/src/http/route_node.dart @@ -110,9 +110,8 @@ class RouteNode { var literalMatcher = (RouteSpecification spec) => spec.segments[level].literal == segmentLiteral; - literalChildren[segmentLiteral] = new RouteNode( - specs.where(literalMatcher).toList(), - level: level + 1); + literalChildren[segmentLiteral] = + new RouteNode(specs.where(literalMatcher).toList(), level: level + 1); specs.removeWhere(literalMatcher); }); diff --git a/lib/src/http/serializable.dart b/lib/src/http/serializable.dart index b5cda9a56..df54bfe88 100644 --- a/lib/src/http/serializable.dart +++ b/lib/src/http/serializable.dart @@ -1,5 +1,3 @@ - - /// Interface for serializable instances to be returned as the HTTP response body. /// /// Implementers of this interface may be the 'body' argument in a [Response]. diff --git a/lib/src/utilities/pbkdf2.dart b/lib/src/utilities/pbkdf2.dart index 23d9a0d5e..288a6e243 100644 --- a/lib/src/utilities/pbkdf2.dart +++ b/lib/src/utilities/pbkdf2.dart @@ -1,5 +1,3 @@ - - /* Based on implementation found here: https://github.com/jamesots/pbkdf2, which contains the following license: Copyright 2014 James Ots diff --git a/lib/src/utilities/test_client.dart b/lib/src/utilities/test_client.dart index 5c4322014..2bae8866f 100644 --- a/lib/src/utilities/test_client.dart +++ b/lib/src/utilities/test_client.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import '../application/application.dart'; import '../application/application_configuration.dart'; - /// Instances of this class are used during testing to make testing an HTTP server more convenient. /// /// A [TestClient] is used to execute HTTP requests during tests. The client is configured to target diff --git a/test/base/controller_test.dart b/test/base/controller_test.dart index 49e91c0c6..f3f40ee77 100644 --- a/test/base/controller_test.dart +++ b/test/base/controller_test.dart @@ -262,19 +262,22 @@ void main() { server = await enableController("/a", TController); var resp = await http.get("http://localhost:4040/a"); expect(resp.statusCode, 200); - expect(ContentType.parse(resp.headers["content-type"]).primaryType, "application"); + expect(ContentType.parse(resp.headers["content-type"]).primaryType, + "application"); expect(ContentType.parse(resp.headers["content-type"]).subType, "json"); }); test("Content-Type can be set adjusting responseContentType", () async { server = await enableController("/a", ContentTypeController); - var resp = await http.get("http://localhost:4040/a?opt=responseContentType"); + var resp = + await http.get("http://localhost:4040/a?opt=responseContentType"); expect(resp.statusCode, 200); expect(resp.headers["content-type"], "text/plain"); expect(resp.body, "body"); }); - test("Content-Type set directly on Response overrides responseContentType", () async { + test("Content-Type set directly on Response overrides responseContentType", + () async { server = await enableController("/a", ContentTypeController); var resp = await http.get("http://localhost:4040/a?opt=direct"); expect(resp.statusCode, 200); @@ -444,9 +447,7 @@ class FilteringController extends HTTPController { } class TController extends HTTPController { - TController() { - - } + TController() {} @httpGet Future getAll() async { return new Response.ok("getAll"); @@ -605,7 +606,8 @@ class ModelEncodeController extends HTTPController { } class ContentTypeController extends HTTPController { - @httpGet getThing(@HTTPQuery("opt") String opt) async { + @httpGet + getThing(@HTTPQuery("opt") String opt) async { if (opt == "responseContentType") { responseContentType = new ContentType("text", "plain"); return new Response.ok("body"); diff --git a/test/base/pipeline_test.dart b/test/base/pipeline_test.dart index ef9cb1a77..b2002fc25 100644 --- a/test/base/pipeline_test.dart +++ b/test/base/pipeline_test.dart @@ -10,8 +10,10 @@ void main() { await app.start(); expect(true, false); } on ApplicationStartupException catch (e) { - expect(e.toString(), - contains("RequestController subclass FailingController instances cannot be reused. Rewrite as .generate(() => new FailingController())")); + expect( + e.toString(), + contains( + "RequestController subclass FailingController instances cannot be reused. Rewrite as .generate(() => new FailingController())")); } }); } diff --git a/test/base/request_controller_test.dart b/test/base/request_controller_test.dart index 070480c85..910479211 100644 --- a/test/base/request_controller_test.dart +++ b/test/base/request_controller_test.dart @@ -148,34 +148,34 @@ void main() { expect(JSON.decode(resp.body), {"name": "Bob"}); }); - test("Responding to request with no content-type, but does have a body, defaults to application/json", () async { + test( + "Responding to request with no content-type, but does have a body, defaults to application/json", + () async { server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); - server - .map((req) => new Request(req)) - .listen((req) async { - var next = new RequestController(); - next.listen((req) async { - return new Response.ok({"a" : "b"}); - }); - await next.receive(req); - }); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok({"a": "b"}); + }); + await next.receive(req); + }); var resp = await http.get("http://localhost:8080"); expect(resp.headers["content-type"], startsWith("application/json")); - expect(JSON.decode(resp.body), {"a" : "b"}); + expect(JSON.decode(resp.body), {"a": "b"}); }); - test("Responding to a request with no explicit content-type and has a body that cannot be encoded to JSON will throw 500", () async { + test( + "Responding to a request with no explicit content-type and has a body that cannot be encoded to JSON will throw 500", + () async { server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); - server - .map((req) => new Request(req)) - .listen((req) async { - var next = new RequestController(); - next.listen((req) async { - return new Response.ok(new DateTime.now()); - }); - await next.receive(req); - }); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(new DateTime.now()); + }); + await next.receive(req); + }); var resp = await http.get("http://localhost:8080"); expect(resp.statusCode, 500); @@ -183,17 +183,17 @@ void main() { expect(resp.body, ""); }); - test("Responding to request with no explicit content-type, but does not have a body, defaults to plaintext Content-Type header", () async { + test( + "Responding to request with no explicit content-type, but does not have a body, defaults to plaintext Content-Type header", + () async { server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); - server - .map((req) => new Request(req)) - .listen((req) async { - var next = new RequestController(); - next.listen((req) async { - return new Response.ok(null); - }); - await next.receive(req); - }); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(null); + }); + await next.receive(req); + }); var resp = await http.get("http://localhost:8080"); expect(resp.statusCode, 200); expect(resp.headers["content-length"], "0"); @@ -203,99 +203,100 @@ void main() { test("Using an encoder that doesn't exist returns a 500", () async { server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); - server - .map((req) => new Request(req)) - .listen((req) async { - var next = new RequestController(); - next.listen((req) async { - return new Response.ok(1234)..contentType = new ContentType("foo", "bar", charset: "utf-8"); - }); - await next.receive(req); - }); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(1234) + ..contentType = new ContentType("foo", "bar", charset: "utf-8"); + }); + await next.receive(req); + }); var resp = await http.get("http://localhost:8080"); var contentType = ContentType.parse(resp.headers["content-type"]); expect(resp.statusCode, 500); expect(contentType.primaryType, "application"); expect(contentType.subType, "json"); - expect(JSON.decode(resp.body), {"error" : "Could not encode body as foo/bar; charset=utf-8."}); + expect(JSON.decode(resp.body), + {"error": "Could not encode body as foo/bar; charset=utf-8."}); }); - test("Using an encoder other than the default correctly encodes and sets content-type", () async { + test( + "Using an encoder other than the default correctly encodes and sets content-type", + () async { server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); - server - .map((req) => new Request(req)) - .listen((req) async { - var next = new RequestController(); - next.listen((req) async { - return new Response.ok(1234)..contentType = new ContentType("text", "plain"); - }); - await next.receive(req); - }); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(1234) + ..contentType = new ContentType("text", "plain"); + }); + await next.receive(req); + }); var resp = await http.get("http://localhost:8080"); expect(resp.statusCode, 200); expect(resp.headers["content-type"], "text/plain"); expect(resp.body, "1234"); }); - test("A decoder with a match-all subtype will be used when matching", () async { + test("A decoder with a match-all subtype will be used when matching", + () async { Response.addEncoder(new ContentType("b", "*"), (s) => s); server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); - server - .map((req) => new Request(req)) - .listen((req) async { - var next = new RequestController(); - next.listen((req) async { - return new Response.ok("hello")..contentType = new ContentType("b", "bar", charset: "utf-8"); - }); - await next.receive(req); - }); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok("hello") + ..contentType = new ContentType("b", "bar", charset: "utf-8"); + }); + await next.receive(req); + }); var resp = await http.get("http://localhost:8080"); expect(resp.statusCode, 200); expect(resp.headers["content-type"], "b/bar; charset=utf-8"); expect(resp.body, "hello"); }); - test("A decoder with a subtype always trumps a decoder that matches any subtype", () async { + test( + "A decoder with a subtype always trumps a decoder that matches any subtype", + () async { Response.addEncoder(new ContentType("a", "*"), (s) => s); Response.addEncoder(new ContentType("a", "html"), (s) { return "$s"; }); server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); - server - .map((req) => new Request(req)) - .listen((req) async { - var next = new RequestController(); - next.listen((req) async { - return new Response.ok("hello")..contentType = new ContentType("a", "html", charset: "utf-8"); - }); - await next.receive(req); - }); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok("hello") + ..contentType = new ContentType("a", "html", charset: "utf-8"); + }); + await next.receive(req); + }); var resp = await http.get("http://localhost:8080"); expect(resp.statusCode, 200); expect(resp.headers["content-type"], "a/html; charset=utf-8"); expect(resp.body, "hello"); }); - test("Using an encoder that blows up during encoded returns 500 safely", () async { + test("Using an encoder that blows up during encoded returns 500 safely", + () async { Response.addEncoder(new ContentType("foo", "bar"), (s) { throw new Exception("uhoh"); }); server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); - server - .map((req) => new Request(req)) - .listen((req) async { - var next = new RequestController(); - next.listen((req) async { - return new Response.ok("hello")..contentType = new ContentType("foo", "bar", charset: "utf-8"); - }); - await next.receive(req); - }); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok("hello") + ..contentType = new ContentType("foo", "bar", charset: "utf-8"); + }); + await next.receive(req); + }); var resp = await http.get("http://localhost:8080"); expect(resp.statusCode, 500); }); } - class SomeObject implements HTTPSerializable { String name; diff --git a/test/utilities/test_client_test.dart b/test/utilities/test_client_test.dart index f0e0925ef..bb68a86f6 100644 --- a/test/utilities/test_client_test.dart +++ b/test/utilities/test_client_test.dart @@ -356,12 +356,13 @@ void main() { test("Can match text object", () async { var defaultTestClient = new TestClient.onPort(4000); - server.queueResponse(new Response.ok("text")..contentType = ContentType.TEXT); + server.queueResponse( + new Response.ok("text")..contentType = ContentType.TEXT); var response = await defaultTestClient.request("/foo").get(); expect(response, hasBody("text")); - server.queueResponse(new Response.ok("text")..contentType = ContentType.TEXT); - + server.queueResponse( + new Response.ok("text")..contentType = ContentType.TEXT); response = await defaultTestClient.request("/foo").get(); try { @@ -376,12 +377,13 @@ void main() { test("Can match JSON Object", () async { var defaultTestClient = new TestClient.onPort(4000); - - server.queueResponse(new Response.ok({"foo" : "bar"})..contentType = ContentType.JSON); + server.queueResponse( + new Response.ok({"foo": "bar"})..contentType = ContentType.JSON); var response = await defaultTestClient.request("/foo").get(); expect(response, hasBody(isNotNull)); - server.queueResponse(new Response.ok({"foo" : "bar"})..contentType = ContentType.JSON); + server.queueResponse( + new Response.ok({"foo": "bar"})..contentType = ContentType.JSON); response = await defaultTestClient.request("/foo").get(); try { expect(response, hasBody({"foo": "notbar"})); @@ -391,7 +393,9 @@ void main() { expect(e.toString(), contains('Body: {"foo":"bar"}')); } - server.queueResponse(new Response.ok({"nocontenttype" : "thatsaysthisisjson"})..contentType = ContentType.TEXT); + server.queueResponse( + new Response.ok({"nocontenttype": "thatsaysthisisjson"}) + ..contentType = ContentType.TEXT); response = await defaultTestClient.request("/foo").get(); try { @@ -523,7 +527,8 @@ void main() { test("Omit status code ignores it", () async { var defaultTestClient = new TestClient.onPort(4000); - server.queueResponse(new Response.ok({"foo" : "bar"})..contentType = ContentType.JSON); + server.queueResponse( + new Response.ok({"foo": "bar"})..contentType = ContentType.JSON); var response = await defaultTestClient.request("/foo").get(); expect( From 17fa85b305fb1274269f7432bb161475904413c8 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 28 Nov 2016 20:13:04 -0500 Subject: [PATCH 14/36] Fixed type on app config --- lib/src/application/application_configuration.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/application/application_configuration.dart b/lib/src/application/application_configuration.dart index 5d9c1e508..4cf99b627 100644 --- a/lib/src/application/application_configuration.dart +++ b/lib/src/application/application_configuration.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'application.dart'; +import '../http/request_sink.dart'; /// A set of values to configure an instance of [Application]. class ApplicationConfiguration { @@ -36,5 +37,5 @@ class ApplicationConfiguration { /// /// Allows delivery of custom configuration parameters to [RequestSink] instances /// that are attached to this application. - Map configurationOptions; + Map configurationOptions; } From 0590b99ce2dfb5efa785140bea5dc38d1afefd3d Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 30 Nov 2016 12:54:07 -0500 Subject: [PATCH 15/36] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 437c8f420..f84a099c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # aqueduct changelog ## 1.0.4 -- Added new `Response.contentType` property. Adding "Content-Type" to the headers of a `Response` no longer has any effect; use this property instead. +- BREAKING CHANGE: Added new `Response.contentType` property. Adding "Content-Type" to the headers of a `Response` no longer has any effect; use this property instead. - `ManagedDataModel`s now scan library dependencies for `ManagedObject` subclasses to generate a data model. ## 1.0.3 From c907b5fb377bb30d59b0006018f2e26375854146 Mon Sep 17 00:00:00 2001 From: Alex Nachlas Date: Wed, 30 Nov 2016 17:01:00 -0500 Subject: [PATCH 16/36] Fixed bug with isNotPresent in partial matcher. (#143) * Fixed bug with isNotPresent in partial matcher. * Added a test, caught another bug, fixed the bug. * Check success case. --- lib/src/utilities/test_matchers.dart | 21 ++++++++++----------- test/utilities/test_client_test.dart | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lib/src/utilities/test_matchers.dart b/lib/src/utilities/test_matchers.dart index 30e6789ff..1bdc90347 100644 --- a/lib/src/utilities/test_matchers.dart +++ b/lib/src/utilities/test_matchers.dart @@ -379,18 +379,17 @@ class _PartialMapMatcher extends Matcher { for (var matchKey in map.keys) { var matchValue = map[matchKey]; var value = item[matchKey]; - - if (value != null && matchValue is _NotPresentMatcher) { - var extra = matchState["extra"]; - if (extra == null) { - extra = []; - matchState["extra"] = extra; + if (matchValue is _NotPresentMatcher) { + if (item.containsKey(matchKey)) { + var extra = matchState["extra"]; + if (extra == null) { + extra = []; + matchState["extra"] = extra; + } + extra = matchKey; + return false; } - extra = matchKey; - return false; - } - - if (value == null) { + } else if (value == null && !matchValue.matches(value, matchState)) { var missing = matchState["missing"]; if (missing == null) { missing = []; diff --git a/test/utilities/test_client_test.dart b/test/utilities/test_client_test.dart index bb68a86f6..a534f9303 100644 --- a/test/utilities/test_client_test.dart +++ b/test/utilities/test_client_test.dart @@ -512,6 +512,34 @@ void main() { expect(e.toString(), contains('Body: {"foo":"bar","x":5}')); } }); + + test("Partial match, null and not present", () async { + var defaultTestClient = new TestClient.onPort(4000); + server.queueResponse(new Response.ok({"foo": null, "bar" : "boo"})); + var response = await defaultTestClient.request("/foo").get(); + expect(response, hasBody(partial({"bar": "boo"}))); + expect(response, hasBody(partial({"foo": isNull}))); + expect(response, hasBody(partial({"baz": isNotPresent}))); + + try { + expect(response, hasBody(partial({"foo": isNotPresent}))); + expect(true, false); + } on TestFailure catch (e) { + expect(e.toString(), + contains("Expected: Body: Partially matches: {foo: ,}")); + expect(e.toString(), contains('Body: {"foo":null,"bar":"boo"}')); + } + try { + expect(response, hasBody(partial({"bar": isNotPresent}))); + expect(true, false); + } on TestFailure catch (e) { + expect( + e.toString(), + contains( + "Expected: Body: Partially matches: {bar: ,}")); + expect(e.toString(), contains('Body: {"foo":null,"bar":"boo"}')); + } + }); }); group("Total matcher", () { From 8c9bef9cda64d8e19f8e6812a2e1fed80de9a54d Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 28 Nov 2016 20:24:41 -0500 Subject: [PATCH 17/36] Cleaned up example requestsink a smidgen --- example/templates/default/lib/wildfire_sink.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/example/templates/default/lib/wildfire_sink.dart b/example/templates/default/lib/wildfire_sink.dart index 176774a6d..fb17cb103 100644 --- a/example/templates/default/lib/wildfire_sink.dart +++ b/example/templates/default/lib/wildfire_sink.dart @@ -20,7 +20,7 @@ class WildfireSink extends RequestSink { /// [Application.configuration] [ApplicationConfiguration.configurationOptions]. /// See bin/start.dart. WildfireSink(Map opts) : super(opts) { - configuration = opts[ConfigurationKey]; + WildfireConfiguration configuration = opts[ConfigurationKey]; LoggingTarget target = opts[LoggingTargetKey]; target?.bind(logger); @@ -33,7 +33,6 @@ class WildfireSink extends RequestSink { ManagedContext context; AuthServer authenticationServer; - WildfireConfiguration configuration; /// All routes must be configured in this method. @override @@ -68,8 +67,7 @@ class WildfireSink extends RequestSink { } ManagedContext contextWithConnectionInfo( - DatabaseConnectionConfiguration database) { - var connectionInfo = configuration.database; + DatabaseConnectionConfiguration connectionInfo) { var dataModel = new ManagedDataModel.fromPackageContainingType(this.runtimeType); var psc = new PostgreSQLPersistentStore.fromConnectionInfo( From fa39ce3aebd1f633289d5945a27bf5bfad1a4516 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 28 Nov 2016 20:25:22 -0500 Subject: [PATCH 18/36] Revert "Cleaned up example requestsink a smidgen" This reverts commit 6255e4dcce61f4fda6113c10b0238aef7b3ef02a. --- example/templates/default/lib/wildfire_sink.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/example/templates/default/lib/wildfire_sink.dart b/example/templates/default/lib/wildfire_sink.dart index fb17cb103..176774a6d 100644 --- a/example/templates/default/lib/wildfire_sink.dart +++ b/example/templates/default/lib/wildfire_sink.dart @@ -20,7 +20,7 @@ class WildfireSink extends RequestSink { /// [Application.configuration] [ApplicationConfiguration.configurationOptions]. /// See bin/start.dart. WildfireSink(Map opts) : super(opts) { - WildfireConfiguration configuration = opts[ConfigurationKey]; + configuration = opts[ConfigurationKey]; LoggingTarget target = opts[LoggingTargetKey]; target?.bind(logger); @@ -33,6 +33,7 @@ class WildfireSink extends RequestSink { ManagedContext context; AuthServer authenticationServer; + WildfireConfiguration configuration; /// All routes must be configured in this method. @override @@ -67,7 +68,8 @@ class WildfireSink extends RequestSink { } ManagedContext contextWithConnectionInfo( - DatabaseConnectionConfiguration connectionInfo) { + DatabaseConnectionConfiguration database) { + var connectionInfo = configuration.database; var dataModel = new ManagedDataModel.fromPackageContainingType(this.runtimeType); var psc = new PostgreSQLPersistentStore.fromConnectionInfo( From 698d4b64e4b728ba969eb1e1379ae94eb9af6112 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 28 Nov 2016 20:27:00 -0500 Subject: [PATCH 19/36] Fixing small thing in template sink --- example/templates/default/lib/wildfire_sink.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/example/templates/default/lib/wildfire_sink.dart b/example/templates/default/lib/wildfire_sink.dart index 176774a6d..fb17cb103 100644 --- a/example/templates/default/lib/wildfire_sink.dart +++ b/example/templates/default/lib/wildfire_sink.dart @@ -20,7 +20,7 @@ class WildfireSink extends RequestSink { /// [Application.configuration] [ApplicationConfiguration.configurationOptions]. /// See bin/start.dart. WildfireSink(Map opts) : super(opts) { - configuration = opts[ConfigurationKey]; + WildfireConfiguration configuration = opts[ConfigurationKey]; LoggingTarget target = opts[LoggingTargetKey]; target?.bind(logger); @@ -33,7 +33,6 @@ class WildfireSink extends RequestSink { ManagedContext context; AuthServer authenticationServer; - WildfireConfiguration configuration; /// All routes must be configured in this method. @override @@ -68,8 +67,7 @@ class WildfireSink extends RequestSink { } ManagedContext contextWithConnectionInfo( - DatabaseConnectionConfiguration database) { - var connectionInfo = configuration.database; + DatabaseConnectionConfiguration connectionInfo) { var dataModel = new ManagedDataModel.fromPackageContainingType(this.runtimeType); var psc = new PostgreSQLPersistentStore.fromConnectionInfo( From e6328274a5e071df54a32a0a1f57d7c7eddb4ac3 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 28 Nov 2016 20:52:52 -0500 Subject: [PATCH 20/36] Renaming some things --- .../default/lib/utilities/auth_delegate.dart | 2 +- .../templates/default/lib/wildfire_sink.dart | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/example/templates/default/lib/utilities/auth_delegate.dart b/example/templates/default/lib/utilities/auth_delegate.dart index c7b000b4d..2ef7f6bff 100644 --- a/example/templates/default/lib/utilities/auth_delegate.dart +++ b/example/templates/default/lib/utilities/auth_delegate.dart @@ -1,6 +1,6 @@ import '../wildfire.dart'; -class WildfireAuthenticationDelegate +class WildfireAuthDelegate implements AuthServerDelegate { Future clientForID(AuthServer server, String id) async { var clientQ = new Query()..matchOn.id = id; diff --git a/example/templates/default/lib/wildfire_sink.dart b/example/templates/default/lib/wildfire_sink.dart index fb17cb103..2e29633df 100644 --- a/example/templates/default/lib/wildfire_sink.dart +++ b/example/templates/default/lib/wildfire_sink.dart @@ -27,12 +27,12 @@ class WildfireSink extends RequestSink { context = contextWithConnectionInfo(configuration.database); - authenticationServer = new AuthServer( - new WildfireAuthenticationDelegate()); + authServer = new AuthServer( + new WildfireAuthDelegate()); } ManagedContext context; - AuthServer authenticationServer; + AuthServer authServer; /// All routes must be configured in this method. @override @@ -40,29 +40,29 @@ class WildfireSink extends RequestSink { router .route("/auth/token") .pipe( - new Authorizer(authenticationServer, strategy: AuthStrategy.client)) - .generate(() => new AuthController(authenticationServer)); + new Authorizer(authServer, strategy: AuthStrategy.client)) + .generate(() => new AuthController(authServer)); router .route("/auth/code") .pipe( - new Authorizer(authenticationServer, strategy: AuthStrategy.client)) - .generate(() => new AuthCodeController(authenticationServer)); + new Authorizer(authServer, strategy: AuthStrategy.client)) + .generate(() => new AuthCodeController(authServer)); router .route("/identity") - .pipe(new Authorizer(authenticationServer)) + .pipe(new Authorizer(authServer)) .generate(() => new IdentityController()); router .route("/register") .pipe( - new Authorizer(authenticationServer, strategy: AuthStrategy.client)) + new Authorizer(authServer, strategy: AuthStrategy.client)) .generate(() => new RegisterController()); router .route("/users/[:id]") - .pipe(new Authorizer(authenticationServer)) + .pipe(new Authorizer(authServer)) .generate(() => new UserController()); } @@ -86,7 +86,7 @@ class WildfireSink extends RequestSink { @override Map documentSecuritySchemes( PackagePathResolver resolver) { - return authenticationServer.documentSecuritySchemes(resolver); + return authServer.documentSecuritySchemes(resolver); } } From 5834dfddc794c96c9de56fcf1ff7546ec7f63a53 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 29 Nov 2016 14:56:29 -0500 Subject: [PATCH 21/36] Made a few static values const --- lib/src/application/isolate_supervisor.dart | 2 +- lib/src/utilities/mock_server.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/application/isolate_supervisor.dart b/lib/src/application/isolate_supervisor.dart index 27841ef05..720c42dcd 100644 --- a/lib/src/application/isolate_supervisor.dart +++ b/lib/src/application/isolate_supervisor.dart @@ -9,7 +9,7 @@ import 'application.dart'; /// /// You should not use this class directly. class ApplicationIsolateSupervisor { - static String MessageStop = "_MessageStop"; + static const String MessageStop = "_MessageStop"; /// Create an isntance of [ApplicationIsolateSupervisor]. ApplicationIsolateSupervisor(this.supervisingApplication, this.isolate, diff --git a/lib/src/utilities/mock_server.dart b/lib/src/utilities/mock_server.dart index 37055d759..6d56434e9 100644 --- a/lib/src/utilities/mock_server.dart +++ b/lib/src/utilities/mock_server.dart @@ -117,14 +117,14 @@ class MockHTTPRequest { /// await nestMockServer.close(); /// }); class MockHTTPServer extends MockServer { - static final int _mockConnectionFailureStatusCode = -1; + static const int _mockConnectionFailureStatusCode = -1; /// Used to simulate a failed request. /// /// Pass this value to [queueResponse] to simulate a 'no response' failure on the next request made to this instance. /// The next request made to this instance will simply not be responded to. /// This is useful in debugging for determining how your code responds to not being able to reach a third party server. - static final Response mockConnectionFailureResponse = + static Response mockConnectionFailureResponse = new Response(_mockConnectionFailureStatusCode, {}, null); MockHTTPServer(this.port) : super(); From a786be73dbb38b31869873dc62d12a9b76f9b889 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 29 Nov 2016 20:48:12 -0500 Subject: [PATCH 22/36] Fixing up some tests, test_all generator --- lib/src/application/isolate_supervisor.dart | 26 ++- test/auth/auth_code_controller_test.dart | 4 +- test/auth/auth_controller_test.dart | 1 + test/base/body_decoder_test.dart | 81 ++++++---- test/base/client_test_test.dart | 2 +- test/base/cors_test.dart | 4 + .../default_resource_controller_test.dart | 14 +- test/db/postgresql/adapter_test.dart | 150 +++++++++--------- test/generate.dart | 57 +++++++ 9 files changed, 218 insertions(+), 121 deletions(-) create mode 100644 test/generate.dart diff --git a/lib/src/application/isolate_supervisor.dart b/lib/src/application/isolate_supervisor.dart index 720c42dcd..2a08b3909 100644 --- a/lib/src/application/isolate_supervisor.dart +++ b/lib/src/application/isolate_supervisor.dart @@ -30,6 +30,7 @@ class ApplicationIsolateSupervisor { /// A reference to the [Logger] used by the [supervisingApplication]. Logger logger; + bool get _isLaunching => _launchCompleter != null; SendPort _serverSendPort; Completer _launchCompleter; Completer _stopCompleter; @@ -42,7 +43,10 @@ class ApplicationIsolateSupervisor { isolate.setErrorsFatal(false); isolate.resume(isolate.pauseCapability); - return _launchCompleter.future.timeout(new Duration(seconds: 30)); + return _launchCompleter.future.timeout(new Duration(seconds: 30), onTimeout: () { + receivePort.close(); + throw new TimeoutException("Isolate failed to launch in 30 seconds."); + }); } /// Stops the [Isolate] being supervised. @@ -50,6 +54,7 @@ class ApplicationIsolateSupervisor { _stopCompleter = new Completer(); _serverSendPort.send(MessageStop); await _stopCompleter.future.timeout(new Duration(seconds: 30)); + receivePort.close(); isolate.kill(); } @@ -65,14 +70,19 @@ class ApplicationIsolateSupervisor { _stopCompleter = null; } else if (message is List) { var stacktrace = new StackTrace.fromString(message.last); + _handleIsolateException(message.first, stacktrace); + } + } + + void _handleIsolateException(dynamic error, StackTrace stacktrace) { + if (_isLaunching) { + receivePort.close(); - if (_launchCompleter != null) { - var appException = new ApplicationStartupException(message.first); - _launchCompleter.completeError(appException, stacktrace); - } else { - var exception = new ApplicationSupervisorException(message.first); - logger.severe("Uncaught exception in isolate.", exception, stacktrace); - } + var appException = new ApplicationStartupException(error); + _launchCompleter.completeError(appException, stacktrace); + } else { + var exception = new ApplicationSupervisorException(error); + logger.severe("Uncaught exception in isolate.", exception, stacktrace); } } diff --git a/test/auth/auth_code_controller_test.dart b/test/auth/auth_code_controller_test.dart index ec72beee9..7e757b166 100644 --- a/test/auth/auth_code_controller_test.dart +++ b/test/auth/auth_code_controller_test.dart @@ -5,16 +5,18 @@ import '../helpers.dart'; void main() { Application application = new Application(); + ManagedContext ctx = null; TestClient client = new TestClient.onPort(8080) ..clientID = "com.stablekernel.app3" ..clientSecret = "mckinley"; tearDownAll(() async { await application?.stop(); + await ctx?.persistentStore?.close(); }); setUpAll(() async { - await contextWithModels([TestUser, Token, AuthCode]); + ctx = await contextWithModels([TestUser, Token, AuthCode]); await application.start(runOnMainIsolate: true); await createUsers(2); diff --git a/test/auth/auth_controller_test.dart b/test/auth/auth_controller_test.dart index 1272d26c0..314f9e368 100644 --- a/test/auth/auth_controller_test.dart +++ b/test/auth/auth_controller_test.dart @@ -23,6 +23,7 @@ void main() { tearDownAll(() async { await server?.close(force: true); + await context?.persistentStore?.close(); }); setUp(() async { diff --git a/test/base/body_decoder_test.dart b/test/base/body_decoder_test.dart index 60557e7cc..18e1c8b6f 100644 --- a/test/base/body_decoder_test.dart +++ b/test/base/body_decoder_test.dart @@ -7,18 +7,21 @@ import 'dart:convert'; void main() { group("Default decoders", () { HttpServer server; + setUp(() async { server = await HttpServer.bind(InternetAddress.ANY_IP_V4, 8123); }); tearDown(() async { - await server?.close(); + await server?.close(force: true); }); test("application/json decoder works on valid json", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/json"}, - body: JSON.encode({"a": "val"})); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/json"}, + body: JSON.encode({"a": "val"})) + .catchError((err) => null); var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -27,9 +30,11 @@ void main() { test("application/x-form-url-encoded decoder works on valid form", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/x-www-form-urlencoded"}, - body: "a=b&c=2"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/x-www-form-urlencoded"}, + body: "a=b&c=2") + .catchError((err) => null); var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -40,8 +45,11 @@ void main() { }); test("Any text decoder works on text", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "text/plain"}, body: "foobar"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "text/plain"}, body: "foobar") + .catchError((err) => null); + var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -49,8 +57,12 @@ void main() { }); test("No found decoder for primary type returns binary", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "notarealthing/nothing"}, body: "foobar"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "notarealthing/nothing"}, + body: "foobar") + .catchError((err) => null); + ; var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -61,7 +73,8 @@ void main() { var req = await new HttpClient() .openUrl("POST", Uri.parse("http://localhost:8123")); req.add("foobar".codeUnits); - req.close(); + req.close().catchError((err) => null); + var request = await server.first; expect(request.headers.contentType, isNull); @@ -71,9 +84,11 @@ void main() { }); test("Decoder that matches primary type but not subtype fails", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/notarealthing"}, - body: "a=b&c=2"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/notarealthing"}, + body: "a=b&c=2") + .catchError((err) => null); var request = await server.first; try { @@ -84,8 +99,10 @@ void main() { }); test("Failed decoding throws exception", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/json"}, body: "{a=b&c=2"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/json"}, body: "{a=b&c=2") + .catchError((err) => null); var request = await server.first; try { @@ -115,13 +132,15 @@ void main() { }); tearDown(() async { - await server?.close(); + await server?.close(force: true); }); test("Added decoder works when content-type matches", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/thingy"}, - body: "this doesn't matter"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/thingy"}, + body: "this doesn't matter") + .catchError((err) => null); var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -129,9 +148,12 @@ void main() { }); test("Added decoder that matches any subtype works", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "somethingelse/whatever"}, - body: "this doesn't matter"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "somethingelse/whatever"}, + body: "this doesn't matter") + .catchError((err) => null); + ; var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -147,13 +169,16 @@ void main() { }); tearDown(() async { - await server?.close(); + await server?.close(force: true); }); test("Subsequent decodes do not re-process body", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/json"}, - body: JSON.encode({"a": "val"})); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/json"}, + body: JSON.encode({"a": "val"})) + .catchError((err) => null); + var request = new Request(await server.first); var b1 = await request.decodeBody(); diff --git a/test/base/client_test_test.dart b/test/base/client_test_test.dart index ac042377a..a6c443a1e 100644 --- a/test/base/client_test_test.dart +++ b/test/base/client_test_test.dart @@ -3,7 +3,7 @@ import 'package:test/test.dart'; import 'dart:io'; import 'dart:async'; -Future main() async { +main() { test("Client can expect array of JSON", () async { TestClient client = new TestClient.onPort(8080); HttpServer server = diff --git a/test/base/cors_test.dart b/test/base/cors_test.dart index db472bb24..bd0184021 100644 --- a/test/base/cors_test.dart +++ b/test/base/cors_test.dart @@ -192,6 +192,7 @@ void main() { await (new HttpClient().open("OPTIONS", "localhost", 8000, "opts")); req.headers.set("Authorization", "Bearer auth"); var resp = await req.close(); + await resp.drain(); expect(resp.statusCode, 200); expectThatNoCORSProcessingOccurred(resp); @@ -203,6 +204,7 @@ void main() { var req = await (new HttpClient().open("OPTIONS", "localhost", 8000, "opts")); var resp = await req.close(); + await resp.drain(); expect(resp.statusCode, 401); expectThatNoCORSProcessingOccurred(resp); @@ -213,6 +215,7 @@ void main() { var req = await (new HttpClient().open("OPTIONS", "localhost", 8000, "foobar")); var resp = await req.close(); + await resp.drain(); expect(resp.statusCode, 404); expectThatNoCORSProcessingOccurred(resp); @@ -224,6 +227,7 @@ void main() { var req = await (new HttpClient() .open("OPTIONS", "localhost", 8000, "nopolicy")); var resp = await req.close(); + await resp.drain(); expect(resp.statusCode, 405); expectThatNoCORSProcessingOccurred(resp); diff --git a/test/base/default_resource_controller_test.dart b/test/base/default_resource_controller_test.dart index b6128be3a..31b9aabae 100644 --- a/test/base/default_resource_controller_test.dart +++ b/test/base/default_resource_controller_test.dart @@ -6,7 +6,7 @@ import '../helpers.dart'; void main() { group("Standard operations", () { - Application app = new Application(); + var app = new Application(); app.configuration.port = 8080; var client = new TestClient.onPort(app.configuration.port); List allObjects = []; @@ -26,6 +26,7 @@ void main() { }); tearDownAll(() async { + await app.mainIsolateSink.context.persistentStore.close(); await app.stop(); }); @@ -82,7 +83,7 @@ void main() { }); group("Standard operation failure cases", () { - Application app = new Application(); + var app = new Application(); app.configuration.port = 8080; var client = new TestClient.onPort(8080); @@ -91,6 +92,7 @@ void main() { }); tearDownAll(() async { + await app.mainIsolateSink.context.persistentStore.close(); await app.stop(); }); @@ -114,8 +116,7 @@ void main() { }); group("Objects that don't exist", () { - Application app = null; - app = new Application(); + var app = new Application(); app.configuration.port = 8080; var client = new TestClient.onPort(8080); @@ -124,6 +125,7 @@ void main() { }); tearDownAll(() async { + await app.mainIsolateSink.context.persistentStore.close(); await app.stop(); }); @@ -150,8 +152,7 @@ void main() { }); group("Extended GET requests", () { - Application app = null; - app = new Application(); + var app = new Application(); app.configuration.port = 8080; var client = new TestClient.onPort(8080); List allObjects = []; @@ -171,6 +172,7 @@ void main() { }); tearDownAll(() async { + await app.mainIsolateSink.context.persistentStore.close(); await app.stop(); }); diff --git a/test/db/postgresql/adapter_test.dart b/test/db/postgresql/adapter_test.dart index bc9413174..2441e4906 100644 --- a/test/db/postgresql/adapter_test.dart +++ b/test/db/postgresql/adapter_test.dart @@ -4,57 +4,53 @@ import 'package:postgres/postgres.dart'; import 'dart:async'; void main() { - PostgreSQLPersistentStore persistentStore = null; - - setUp(() { - persistentStore = new PostgreSQLPersistentStore(() async { - var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); - await connection.open(); - return connection; - }); - }); + PostgreSQLPersistentStore persistentStore = new PostgreSQLPersistentStore(() async { + var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); + await connection.open(); + return connection; + });; tearDown(() async { await persistentStore.close(); }); - test("A down connection will restart", () async { - var result = await persistentStore.execute("select 1"); - expect(result, [ - [1] - ]); - - await persistentStore.close(); - - result = await persistentStore.execute("select 1"); - expect(result, [ - [1] - ]); - }); - - test("Ask for multiple connections at once, yield one successful connection", - () async { - var connections = await Future.wait([1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - .map((_) => persistentStore.getDatabaseConnection())); - var first = connections.first; - expect(connections, everyElement(first)); - }); - - test("Make multiple requests at once, yield one successful connection", - () async { - var expectedValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - var values = await Future - .wait(expectedValues.map((i) => persistentStore.execute("select $i"))); - - expect( - values, - expectedValues - .map((v) => [ - [v] - ]) - .toList()); - }); +// test("A down connection will restart", () async { +// var result = await persistentStore.execute("select 1"); +// expect(result, [ +// [1] +// ]); +// +// await persistentStore.close(); +// +// result = await persistentStore.execute("select 1"); +// expect(result, [ +// [1] +// ]); +// }); +// +// test("Ask for multiple connections at once, yield one successful connection", +// () async { +// var connections = await Future.wait([1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +// .map((_) => persistentStore.getDatabaseConnection())); +// var first = connections.first; +// expect(connections, everyElement(first)); +// }); +// +// test("Make multiple requests at once, yield one successful connection", +// () async { +// var expectedValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +// var values = await Future +// .wait(expectedValues.map((i) => persistentStore.execute("select $i"))); +// +// expect( +// values, +// expectedValues +// .map((v) => [ +// [v] +// ]) +// .toList()); +// }); test("Make multiple requests at once, all fail because db connect fails", () async { @@ -70,35 +66,35 @@ void main() { .map((i) => persistentStore.execute("select $i").catchError((e) => e))); expect(values, everyElement(new isInstanceOf())); }); - - test( - "Make multiple requests at once, first few fails because db connect fails (but eventually succeeds)", - () async { - var counter = 0; - persistentStore = new PostgreSQLPersistentStore(() async { - var conn = (counter == 0 - ? new PostgreSQLConnection("localhost", 5432, "xyzxyznotadb", - username: "dart", password: "dart") - : new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart")); - counter++; - await conn.open(); - return conn; - }); - var expectedValues = [1, 2, 3, 4, 5]; - var values = await Future.wait(expectedValues - .map((i) => persistentStore.execute("select $i").catchError((e) => e))); - expect(values, everyElement(new isInstanceOf())); - - expectedValues = [5, 6, 7, 8, 9]; - values = await Future - .wait(expectedValues.map((i) => persistentStore.execute("select $i"))); - expect( - values, - expectedValues - .map((v) => [ - [v] - ]) - .toList()); - }); +// +// test( +// "Make multiple requests at once, first few fails because db connect fails (but eventually succeeds)", +// () async { +// var counter = 0; +// persistentStore = new PostgreSQLPersistentStore(() async { +// var conn = (counter == 0 +// ? new PostgreSQLConnection("localhost", 5432, "xyzxyznotadb", +// username: "dart", password: "dart") +// : new PostgreSQLConnection("localhost", 5432, "dart_test", +// username: "dart", password: "dart")); +// counter++; +// await conn.open(); +// return conn; +// }); +// var expectedValues = [1, 2, 3, 4, 5]; +// var values = await Future.wait(expectedValues +// .map((i) => persistentStore.execute("select $i").catchError((e) => e))); +// expect(values, everyElement(new isInstanceOf())); +// +// expectedValues = [5, 6, 7, 8, 9]; +// values = await Future +// .wait(expectedValues.map((i) => persistentStore.execute("select $i"))); +// expect( +// values, +// expectedValues +// .map((v) => [ +// [v] +// ]) +// .toList()); +// }); } diff --git a/test/generate.dart b/test/generate.dart new file mode 100644 index 000000000..e2ac31bc8 --- /dev/null +++ b/test/generate.dart @@ -0,0 +1,57 @@ +import 'dart:io'; +import 'package:path/path.dart' as path_lib; + + +void main(List args) { + var testDirectory = new Directory(path_lib.relative("test", + from: Directory.current.path)); + + var allTestFiles = testFilesFromDirectory(testDirectory) + .map((f) => path_lib.relative(f.path, from: testDirectory.path)) + .toList(); + + var buf = new StringBuffer(); + + buf.writeln("import 'package:test/test.dart';"); + allTestFiles.forEach((filename) { + buf.writeln("import '$filename' as ${prefixForFilename(filename)};"); + }); + + buf.writeln(""); + buf.writeln("void main() {"); + allTestFiles.forEach((filename) { + buf.writeln("\tgroup('$filename', ${prefixForFilename(filename)}.main);"); + }); + buf.writeln("}"); + + var outPath = path_lib.join(testDirectory.absolute.path, args.first); + var outFile = new File(outPath); + outFile.writeAsStringSync(buf.toString()); +} + +String prefixForFilename(String filename) { + return path_lib + .split(filename) + .join("_") + .split(".dart") + .first; +} + +List testFilesFromDirectory(Directory dir) { + var entries = dir.listSync(); + var files = entries + .where((fse) => fse is File) + .where((fse) => fse.path.endsWith("_test.dart")) + .map((fse) => fse as File) + .toList(); + + var subdirectoryFiles = entries + .where((fse) => fse is Directory) + .map((dir) => testFilesFromDirectory(dir)) + .expand((files) => files) + .toList(); + + subdirectoryFiles.addAll(files); + + return subdirectoryFiles; +} \ No newline at end of file From 872c4e51e466b8882f5173a24bc08b2439de170d Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 29 Nov 2016 22:18:31 -0500 Subject: [PATCH 23/36] Updated some tests, made the last managedcontext the default context --- lib/src/db/managed/context.dart | 16 ++- test/auth/auth_controller_test.dart | 20 ++-- test/base/controller_test.dart | 7 +- test/db/model_controller_test.dart | 1 + test/db/model_test.dart | 10 +- test/db/postgresql/adapter_test.dart | 145 ++++++++++++++------------- test/db/query_test.dart | 8 +- 7 files changed, 116 insertions(+), 91 deletions(-) diff --git a/lib/src/db/managed/context.dart b/lib/src/db/managed/context.dart index 9cc3fc794..ce3f726cb 100644 --- a/lib/src/db/managed/context.dart +++ b/lib/src/db/managed/context.dart @@ -19,7 +19,8 @@ class ManagedContext { /// For classes that require a [ManagedContext] - like [Query] - this is the default context when none /// is specified. /// - /// This value is set when the first [ManagedContext] instantiated in an isolate. Most applications + /// This value is set when a [ManagedContext] instantiated in an isolate; the last context created + /// is the default context. Most applications /// will not use more than one [ManagedContext]. When running tests, you should set /// this value each time you instantiate a [ManagedContext] to ensure that a previous test isolate /// state did not set this property. @@ -27,13 +28,18 @@ class ManagedContext { /// Creates an instance of [ManagedContext] from a [ManagedDataModel] and [PersistentStore]. /// - /// If this is the first [ManagedContext] instantiated on an isolate, this instance will because the [ManagedContext.defaultContext]. + /// This instance will become the [ManagedContext.defaultContext], unless another [ManagedContext] + /// is created, in which the new context becomes the default context. ManagedContext(this.dataModel, this.persistentStore) { - if (defaultContext == null) { - defaultContext = this; - } + defaultContext = this; } + /// Creates an instance of [ManagedContext] from a [ManagedDataModel] and [PersistentStore]. + /// + /// This constructor creates an instance in the same way the default constructor does, + /// but does not set it to be the [defaultContext]. + ManagedContext.standalone(this.dataModel, this.persistentStore); + /// The persistent store that [Query]s on this context are executed on. PersistentStore persistentStore; diff --git a/test/auth/auth_controller_test.dart b/test/auth/auth_controller_test.dart index 314f9e368..f5d034c27 100644 --- a/test/auth/auth_controller_test.dart +++ b/test/auth/auth_controller_test.dart @@ -5,21 +5,23 @@ import 'dart:convert'; import '../helpers.dart'; void main() { - RequestController.includeErrorDetailsInServerErrorResponses = true; - ManagedContext context = null; HttpServer server; TestClient client = new TestClient.onPort(8080) ..clientID = "com.stablekernel.app1" ..clientSecret = "kilimanjaro"; + AuthServer authenticationServer; + Router router; + + setUpAll(() { + authenticationServer = new AuthServer(new AuthDelegate(context)); - var authenticationServer = - new AuthServer(new AuthDelegate(context)); - var router = new Router(); - router - .route("/auth/token") - .generate(() => new AuthController(authenticationServer)); - router.finalize(); + router = new Router(); + router + .route("/auth/token") + .generate(() => new AuthController(authenticationServer)); + router.finalize(); + }); tearDownAll(() async { await server?.close(force: true); diff --git a/test/base/controller_test.dart b/test/base/controller_test.dart index f3f40ee77..04fcc42a4 100644 --- a/test/base/controller_test.dart +++ b/test/base/controller_test.dart @@ -11,8 +11,11 @@ import '../helpers.dart'; void main() { HttpServer server; - ManagedDataModel dm = new ManagedDataModel([TestModel]); - ManagedContext _ = new ManagedContext(dm, new DefaultPersistentStore()); + + setUpAll(() { + new ManagedContext(new ManagedDataModel([TestModel]), + new DefaultPersistentStore()); + }); tearDown(() async { await server?.close(force: true); diff --git a/test/db/model_controller_test.dart b/test/db/model_controller_test.dart index d95356886..4ae34d6e3 100644 --- a/test/db/model_controller_test.dart +++ b/test/db/model_controller_test.dart @@ -24,6 +24,7 @@ main() { }); tearDownAll(() async { + await context.persistentStore.close(); await server?.close(force: true); }); diff --git a/test/db/model_test.dart b/test/db/model_test.dart index 695459e4f..818bac545 100644 --- a/test/db/model_test.dart +++ b/test/db/model_test.dart @@ -4,10 +4,12 @@ import 'dart:mirrors'; import '../helpers.dart'; void main() { - var ps = new DefaultPersistentStore(); - ManagedDataModel dm = - new ManagedDataModel([TransientTest, TransientTypeTest, User, Post]); - ManagedContext _ = new ManagedContext(dm, ps); + setUpAll(() { + var ps = new DefaultPersistentStore(); + ManagedDataModel dm = + new ManagedDataModel([TransientTest, TransientTypeTest, User, Post]); + var _ = new ManagedContext(dm, ps); + }); test("NoSuchMethod still throws", () { var user = new User(); diff --git a/test/db/postgresql/adapter_test.dart b/test/db/postgresql/adapter_test.dart index 2441e4906..b046e5523 100644 --- a/test/db/postgresql/adapter_test.dart +++ b/test/db/postgresql/adapter_test.dart @@ -15,42 +15,42 @@ void main() { await persistentStore.close(); }); -// test("A down connection will restart", () async { -// var result = await persistentStore.execute("select 1"); -// expect(result, [ -// [1] -// ]); -// -// await persistentStore.close(); -// -// result = await persistentStore.execute("select 1"); -// expect(result, [ -// [1] -// ]); -// }); -// -// test("Ask for multiple connections at once, yield one successful connection", -// () async { -// var connections = await Future.wait([1, 2, 3, 4, 5, 6, 7, 8, 9, 10] -// .map((_) => persistentStore.getDatabaseConnection())); -// var first = connections.first; -// expect(connections, everyElement(first)); -// }); -// -// test("Make multiple requests at once, yield one successful connection", -// () async { -// var expectedValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; -// var values = await Future -// .wait(expectedValues.map((i) => persistentStore.execute("select $i"))); -// -// expect( -// values, -// expectedValues -// .map((v) => [ -// [v] -// ]) -// .toList()); -// }); + test("A down connection will restart", () async { + var result = await persistentStore.execute("select 1"); + expect(result, [ + [1] + ]); + + await persistentStore.close(); + + result = await persistentStore.execute("select 1"); + expect(result, [ + [1] + ]); + }); + + test("Ask for multiple connections at once, yield one successful connection", + () async { + var connections = await Future.wait([1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + .map((_) => persistentStore.getDatabaseConnection())); + var first = connections.first; + expect(connections, everyElement(first)); + }); + + test("Make multiple requests at once, yield one successful connection", + () async { + var expectedValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var values = await Future + .wait(expectedValues.map((i) => persistentStore.execute("select $i"))); + + expect( + values, + expectedValues + .map((v) => [ + [v] + ]) + .toList()); + }); test("Make multiple requests at once, all fail because db connect fails", () async { @@ -58,7 +58,11 @@ void main() { var connection = new PostgreSQLConnection( "localhost", 5432, "xyzxyznotadb", username: "dart", password: "dart"); - await connection.open(); + try { + await connection.open(); + } catch (e) { + await connection.close(); + } return connection; }); var expectedValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; @@ -66,35 +70,40 @@ void main() { .map((i) => persistentStore.execute("select $i").catchError((e) => e))); expect(values, everyElement(new isInstanceOf())); }); -// -// test( -// "Make multiple requests at once, first few fails because db connect fails (but eventually succeeds)", -// () async { -// var counter = 0; -// persistentStore = new PostgreSQLPersistentStore(() async { -// var conn = (counter == 0 -// ? new PostgreSQLConnection("localhost", 5432, "xyzxyznotadb", -// username: "dart", password: "dart") -// : new PostgreSQLConnection("localhost", 5432, "dart_test", -// username: "dart", password: "dart")); -// counter++; -// await conn.open(); -// return conn; -// }); -// var expectedValues = [1, 2, 3, 4, 5]; -// var values = await Future.wait(expectedValues -// .map((i) => persistentStore.execute("select $i").catchError((e) => e))); -// expect(values, everyElement(new isInstanceOf())); -// -// expectedValues = [5, 6, 7, 8, 9]; -// values = await Future -// .wait(expectedValues.map((i) => persistentStore.execute("select $i"))); -// expect( -// values, -// expectedValues -// .map((v) => [ -// [v] -// ]) -// .toList()); -// }); + + test( + "Make multiple requests at once, first few fails because db connect fails (but eventually succeeds)", + () async { + var counter = 0; + persistentStore = new PostgreSQLPersistentStore(() async { + var connection = (counter == 0 + ? new PostgreSQLConnection("localhost", 5432, "xyzxyznotadb", + username: "dart", password: "dart") + : new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart")); + counter++; + try { + await connection.open(); + } catch (e) { + await connection.close(); + } + + return connection; + }); + var expectedValues = [1, 2, 3, 4, 5]; + var values = await Future.wait(expectedValues + .map((i) => persistentStore.execute("select $i").catchError((e) => e))); + expect(values, everyElement(new isInstanceOf())); + + expectedValues = [5, 6, 7, 8, 9]; + values = await Future + .wait(expectedValues.map((i) => persistentStore.execute("select $i"))); + expect( + values, + expectedValues + .map((v) => [ + [v] + ]) + .toList()); + }); } diff --git a/test/db/query_test.dart b/test/db/query_test.dart index ec4c78a0d..0a4df9951 100644 --- a/test/db/query_test.dart +++ b/test/db/query_test.dart @@ -3,9 +3,11 @@ import 'package:test/test.dart'; import '../helpers.dart'; main() { - var ps = new DefaultPersistentStore(); - ManagedDataModel dm = new ManagedDataModel([TestModel]); - ManagedContext _ = new ManagedContext(dm, ps); + setUpAll(() { + var ps = new DefaultPersistentStore(); + ManagedDataModel dm = new ManagedDataModel([TestModel]); + ManagedContext _ = new ManagedContext(dm, ps); + }); test("Accessing valueObject of Query automatically creates an instance", () { var q = new Query()..values.id = 1; From cb7dce00ed841b57dee728991598e345144d466a Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 29 Nov 2016 22:18:47 -0500 Subject: [PATCH 24/36] Removed unused method --- lib/src/http/route_node.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/src/http/route_node.dart b/lib/src/http/route_node.dart index 31d95f4db..c9fcacb16 100644 --- a/lib/src/http/route_node.dart +++ b/lib/src/http/route_node.dart @@ -45,24 +45,6 @@ class RouteSegment { bool get isVariable => variableName != null; bool isRemainingMatcher = false; - bool matches(String pathSegment) { - if (isLiteralMatcher) { - return pathSegment == literal; - } - - if (hasRegularExpression) { - if (matcher.firstMatch(pathSegment) == null) { - return false; - } - } - - if (isVariable) { - return true; - } - - return false; - } - bool operator ==(dynamic other) { return literal == other.literal && variableName == other.variableName && From fc32de3af562a4749fa2edeb94739d063db9545d Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 29 Nov 2016 23:14:19 -0500 Subject: [PATCH 25/36] Added instructions for running tests and collecting coverage --- CONTRIBUTING.md | 31 +++++++++++++++++++ .../generate_test_all.dart | 0 2 files changed, 31 insertions(+) create mode 100644 CONTRIBUTING.md rename test/generate.dart => tool/generate_test_all.dart (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..b1e5a7876 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +### Running Tests + +A local database must exist, configured using the same script in .travis.yml. + +Tests are run with the following command: + + pub run test -j 1 + +### Collecting Coverage + +Install code_coverage: + + pub global activate coverage + +Run the following script: + + dart tool/generate_test_all.dart test/test_all.dart + +Then, run this generated script: + + dart --observe --checked test/test_all.dart + +From another terminal, run collect coverage (replace port if the previous script reported using another port: + + collect_coverage --port=8181 -o coverage.json --resume-isolates + +Once this command completes, format the coverage.json file into lcov: + + format_coverage --packages=.packages -l --report-on=lib -o coverage.lcov -i coverage.json + +This file is best viewed using Atom and the lcov-info package. \ No newline at end of file diff --git a/test/generate.dart b/tool/generate_test_all.dart similarity index 100% rename from test/generate.dart rename to tool/generate_test_all.dart From 0b75d3fa470a61b691a37a0b4e9631d4c16a275d Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 29 Nov 2016 23:49:33 -0500 Subject: [PATCH 26/36] UPdated change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f84a099c9..f3b848745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.0.4 - BREAKING CHANGE: Added new `Response.contentType` property. Adding "Content-Type" to the headers of a `Response` no longer has any effect; use this property instead. - `ManagedDataModel`s now scan library dependencies for `ManagedObject` subclasses to generate a data model. +- The *last* instantiated `ManagedContext` now becomes the `ManagedContext.defaultContext`; prior to this change, it was the first instantiated context. Added `ManagedContext.standalone` to opt out of setting the default context. ## 1.0.3 - Fix to allow Windows user to use `aqueduct setup`. From d5ca1f0e30132bba25bede1ee83d1ac53b84f19f Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 30 Nov 2016 11:49:24 -0500 Subject: [PATCH 27/36] Using codecov and adding to travis --- .travis.yml | 9 ++- .../isolate_application_server.dart | 21 ++++--- pubspec.yaml | 1 + test/base/client_test_test.dart | 1 - test/base/isolate_application_test.dart | 22 +++---- tool/generate_test_all.dart | 57 ------------------- 6 files changed, 31 insertions(+), 80 deletions(-) delete mode 100644 tool/generate_test_all.dart diff --git a/.travis.yml b/.travis.yml index 751dd43cf..0fb321009 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,14 @@ before_script: - psql -c "alter user dart with password 'dart';" -U postgres - psql -c 'grant all on database dart_test to dart;' -U postgres - pub get -script: pub run test -j 1 -r expanded +script: + - pub run test -j 1 -r expanded + - pub global activate -sgit https://github.com/stablekernel/codecov_dart.git + - dart_codecov_generator --report-on=lib/ --verbose --no-html +after_success: + - if [ "$TRAVIS_BRANCH" == "master" ]; then + bash <(curl -s https://codecov.io/bash) + fi branches: only: - master diff --git a/lib/src/application/isolate_application_server.dart b/lib/src/application/isolate_application_server.dart index 967de3b6e..1477ed324 100644 --- a/lib/src/application/isolate_application_server.dart +++ b/lib/src/application/isolate_application_server.dart @@ -41,16 +41,23 @@ class ApplicationIsolateServer extends ApplicationServer { /// This method is used internally. void isolateServerEntryPoint(ApplicationInitialServerMessage params) { - var sinkSourceLibraryMirror = - currentMirrorSystem().libraries[params.streamLibraryURI]; - var sinkTypeMirror = sinkSourceLibraryMirror.declarations[ - new Symbol(params.streamTypeName)] as ClassMirror; + RequestSink sink; + try { + var sinkSourceLibraryMirror = + currentMirrorSystem().libraries[params.streamLibraryURI]; + var sinkTypeMirror = sinkSourceLibraryMirror.declarations[ + new Symbol(params.streamTypeName)] as ClassMirror; - var app = sinkTypeMirror.newInstance( - new Symbol(""), [params.configuration.configurationOptions]).reflectee; + sink = sinkTypeMirror.newInstance( + new Symbol(""), [params.configuration.configurationOptions]).reflectee; + } catch (e, st) { + params.parentMessagePort.send([e, st.toString()]); + + return; + } var server = new ApplicationIsolateServer( - app, params.configuration, params.identifier, params.parentMessagePort); + sink, params.configuration, params.identifier, params.parentMessagePort); server.start(shareHttpServer: true); } diff --git a/pubspec.yaml b/pubspec.yaml index 312e497ed..c391f4437 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: dev_dependencies: test: any http: ">=0.11.3+7 <0.12.0" + coverage: any executables: aqueduct: aqueduct diff --git a/test/base/client_test_test.dart b/test/base/client_test_test.dart index a6c443a1e..dc965b4eb 100644 --- a/test/base/client_test_test.dart +++ b/test/base/client_test_test.dart @@ -1,7 +1,6 @@ import 'package:aqueduct/aqueduct.dart'; import 'package:test/test.dart'; import 'dart:io'; -import 'dart:async'; main() { test("Client can expect array of JSON", () async { diff --git a/test/base/isolate_application_test.dart b/test/base/isolate_application_test.dart index 8bac1a1a7..c16a45083 100644 --- a/test/base/isolate_application_test.dart +++ b/test/base/isolate_application_test.dart @@ -82,39 +82,35 @@ main() { () async { var crashingApp = new Application(); - var succeeded = false; try { crashingApp.configuration.configurationOptions = { "crashIn": "constructor" }; await crashingApp.start(); - succeeded = true; - } catch (e) { + expect(true, false); + } on ApplicationStartupException catch (e) { expect(e.toString(), contains("TestException: constructor")); } - expect(succeeded, false); try { crashingApp.configuration.configurationOptions = { "crashIn": "addRoutes" }; await crashingApp.start(); - succeeded = true; - } catch (e) { + expect(true, false); + } on ApplicationStartupException catch (e) { expect(e.toString(), contains("TestException: addRoutes")); } - expect(succeeded, false); try { crashingApp.configuration.configurationOptions = { "crashIn": "willOpen" }; await crashingApp.start(); - succeeded = true; - } catch (e) { + expect(true, false); + } on ApplicationStartupException catch (e) { expect(e.toString(), contains("TestException: willOpen")); } - expect(succeeded, false); crashingApp.configuration.configurationOptions = {"crashIn": "dontCrash"}; await crashingApp.start(); @@ -132,14 +128,12 @@ main() { var conflictingApp = new Application(); conflictingApp.configuration.port = 8080; - var successful = false; try { await conflictingApp.start(); - successful = true; - } catch (e) { + expect(true, false); + } on ApplicationStartupException catch (e) { expect(e, new isInstanceOf()); } - expect(successful, false); await server.close(force: true); await conflictingApp.stop(); diff --git a/tool/generate_test_all.dart b/tool/generate_test_all.dart deleted file mode 100644 index e2ac31bc8..000000000 --- a/tool/generate_test_all.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'dart:io'; -import 'package:path/path.dart' as path_lib; - - -void main(List args) { - var testDirectory = new Directory(path_lib.relative("test", - from: Directory.current.path)); - - var allTestFiles = testFilesFromDirectory(testDirectory) - .map((f) => path_lib.relative(f.path, from: testDirectory.path)) - .toList(); - - var buf = new StringBuffer(); - - buf.writeln("import 'package:test/test.dart';"); - allTestFiles.forEach((filename) { - buf.writeln("import '$filename' as ${prefixForFilename(filename)};"); - }); - - buf.writeln(""); - buf.writeln("void main() {"); - allTestFiles.forEach((filename) { - buf.writeln("\tgroup('$filename', ${prefixForFilename(filename)}.main);"); - }); - buf.writeln("}"); - - var outPath = path_lib.join(testDirectory.absolute.path, args.first); - var outFile = new File(outPath); - outFile.writeAsStringSync(buf.toString()); -} - -String prefixForFilename(String filename) { - return path_lib - .split(filename) - .join("_") - .split(".dart") - .first; -} - -List testFilesFromDirectory(Directory dir) { - var entries = dir.listSync(); - var files = entries - .where((fse) => fse is File) - .where((fse) => fse.path.endsWith("_test.dart")) - .map((fse) => fse as File) - .toList(); - - var subdirectoryFiles = entries - .where((fse) => fse is Directory) - .map((dir) => testFilesFromDirectory(dir)) - .expand((files) => files) - .toList(); - - subdirectoryFiles.addAll(files); - - return subdirectoryFiles; -} \ No newline at end of file From 5101d6e8519600460a4a1a5e80da1a3c81af2ca2 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Thu, 1 Dec 2016 08:50:30 -0500 Subject: [PATCH 28/36] pr feedback --- CONTRIBUTING.md | 24 ------------------------ lib/src/db/managed/context.dart | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1e5a7876..6d0adc87b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,27 +5,3 @@ A local database must exist, configured using the same script in .travis.yml. Tests are run with the following command: pub run test -j 1 - -### Collecting Coverage - -Install code_coverage: - - pub global activate coverage - -Run the following script: - - dart tool/generate_test_all.dart test/test_all.dart - -Then, run this generated script: - - dart --observe --checked test/test_all.dart - -From another terminal, run collect coverage (replace port if the previous script reported using another port: - - collect_coverage --port=8181 -o coverage.json --resume-isolates - -Once this command completes, format the coverage.json file into lcov: - - format_coverage --packages=.packages -l --report-on=lib -o coverage.lcov -i coverage.json - -This file is best viewed using Atom and the lcov-info package. \ No newline at end of file diff --git a/lib/src/db/managed/context.dart b/lib/src/db/managed/context.dart index ce3f726cb..665812f5d 100644 --- a/lib/src/db/managed/context.dart +++ b/lib/src/db/managed/context.dart @@ -19,7 +19,7 @@ class ManagedContext { /// For classes that require a [ManagedContext] - like [Query] - this is the default context when none /// is specified. /// - /// This value is set when a [ManagedContext] instantiated in an isolate; the last context created + /// This value is set when a [ManagedContext] is instantiated in an isolate; the last context created /// is the default context. Most applications /// will not use more than one [ManagedContext]. When running tests, you should set /// this value each time you instantiate a [ManagedContext] to ensure that a previous test isolate From 86deab99371a232e0852f29a3f5a71b5b2c46465 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Sun, 4 Dec 2016 10:57:19 -0500 Subject: [PATCH 29/36] Jc/fix managed resolution (#147) * Tests and fixes for template generation and data model generation * Fixed some exit code stuff * Updated changelog * dartfmt * Removed unused imports --- .analysis_options | 4 +- CHANGELOG.md | 2 +- bin/aqueduct.dart | 21 ++-- .../templates/default/lib/wildfire_sink.dart | 2 +- lib/src/application/isolate_supervisor.dart | 3 +- lib/src/commands/cli_command.dart | 3 +- lib/src/commands/template_creator.dart | 23 +++- lib/src/db/managed/data_model.dart | 72 ++++++------ lib/src/db/schema/migration.dart | 6 +- test/auth/auth_controller_test.dart | 3 +- test/base/controller_test.dart | 4 +- test/command/create_test.dart | 110 ++++++++++++++++++ test/db/model_test.dart | 2 +- test/db/postgresql/adapter_test.dart | 6 +- test/test_project/lib/src/wildfire_sink.dart | 3 +- test/utilities/test_client_test.dart | 8 +- 16 files changed, 203 insertions(+), 69 deletions(-) create mode 100644 test/command/create_test.dart diff --git a/.analysis_options b/.analysis_options index 518eb901a..f4bbaee15 100644 --- a/.analysis_options +++ b/.analysis_options @@ -1,2 +1,4 @@ analyzer: - strong-mode: true \ No newline at end of file + strong-mode: true + exclude: + - tmp_templates/** \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f3b848745..dda9a7df1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.0.4 - BREAKING CHANGE: Added new `Response.contentType` property. Adding "Content-Type" to the headers of a `Response` no longer has any effect; use this property instead. -- `ManagedDataModel`s now scan library dependencies for `ManagedObject` subclasses to generate a data model. +- `ManagedDataModel`s now scan all libraries for `ManagedObject` subclasses to generate a data model. Use `ManagedDataModel.fromCurrentMirrorSystem` to create instances of `ManagedDataModel`. - The *last* instantiated `ManagedContext` now becomes the `ManagedContext.defaultContext`; prior to this change, it was the first instantiated context. Added `ManagedContext.standalone` to opt out of setting the default context. ## 1.0.3 diff --git a/bin/aqueduct.dart b/bin/aqueduct.dart index 4dc8bb5c9..1b87b0959 100644 --- a/bin/aqueduct.dart +++ b/bin/aqueduct.dart @@ -1,8 +1,9 @@ -import 'dart:async'; -import 'package:args/args.dart'; +import 'dart:io'; + import 'package:aqueduct/aqueduct.dart'; +import 'package:args/args.dart'; -Future main(List args) async { +main(List args) async { var templateCreator = new CLITemplateCreator(); var migrationRunner = new CLIMigrationRunner(); var setupCommand = new CLISetup(); @@ -19,16 +20,20 @@ Future main(List args) async { if (values.command == null) { print( "Invalid command, options are: ${totalParser.commands.keys.join(", ")}"); - return -1; + exitCode = 1; + return; } else if (values.command.name == "create") { - return await templateCreator.process(values.command); + exitCode = await templateCreator.process(values.command); + return; } else if (values.command.name == "db") { - return await migrationRunner.process(values.command); + exitCode = await migrationRunner.process(values.command); + return; } else if (values.command.name == "setup") { - return await setupCommand.process(values.command); + exitCode = await setupCommand.process(values.command); + return; } print( "Invalid command, options are: ${totalParser.commands.keys.join(", ")}"); - return -1; + exitCode = 1; } diff --git a/example/templates/default/lib/wildfire_sink.dart b/example/templates/default/lib/wildfire_sink.dart index 2e29633df..281e7e563 100644 --- a/example/templates/default/lib/wildfire_sink.dart +++ b/example/templates/default/lib/wildfire_sink.dart @@ -69,7 +69,7 @@ class WildfireSink extends RequestSink { ManagedContext contextWithConnectionInfo( DatabaseConnectionConfiguration connectionInfo) { var dataModel = - new ManagedDataModel.fromPackageContainingType(this.runtimeType); + new ManagedDataModel.fromCurrentMirrorSystem(); var psc = new PostgreSQLPersistentStore.fromConnectionInfo( connectionInfo.username, connectionInfo.password, diff --git a/lib/src/application/isolate_supervisor.dart b/lib/src/application/isolate_supervisor.dart index 2a08b3909..49a1ac1dc 100644 --- a/lib/src/application/isolate_supervisor.dart +++ b/lib/src/application/isolate_supervisor.dart @@ -43,7 +43,8 @@ class ApplicationIsolateSupervisor { isolate.setErrorsFatal(false); isolate.resume(isolate.pauseCapability); - return _launchCompleter.future.timeout(new Duration(seconds: 30), onTimeout: () { + return _launchCompleter.future.timeout(new Duration(seconds: 30), + onTimeout: () { receivePort.close(); throw new TimeoutException("Isolate failed to launch in 30 seconds."); }); diff --git a/lib/src/commands/cli_command.dart b/lib/src/commands/cli_command.dart index f1e1bdb7f..a6bd8e3fc 100644 --- a/lib/src/commands/cli_command.dart +++ b/lib/src/commands/cli_command.dart @@ -35,6 +35,7 @@ abstract class CLICommand { } finally { await cleanup(); } - return -1; + + return 1; } } diff --git a/lib/src/commands/template_creator.dart b/lib/src/commands/template_creator.dart index 49fc76892..6a06abfca 100644 --- a/lib/src/commands/template_creator.dart +++ b/lib/src/commands/template_creator.dart @@ -36,24 +36,24 @@ class CLITemplateCreator extends CLICommand { Future handle(ArgResults argValues) async { if (argValues["help"] == true) { print("${options.usage}"); - return 0; + return 1; } if (argValues["name"] == null) { print("No project name specified.\n\n${options.usage}"); - return -1; + return 1; } if (argValues["name"] == null || !isSnakeCase(argValues["name"])) { print( "Invalid project name ${argValues["name"]} is not snake_case).\n\n${options.usage}"); - return -1; + return 1; } var destDirectory = destinationDirectoryFromPath(argValues["name"]); if (destDirectory.existsSync()) { print("${destDirectory.path} already exists, stopping."); - return -1; + return 1; } destDirectory.createSync(); @@ -75,7 +75,7 @@ class CLITemplateCreator extends CLICommand { } if (!sourceDirectory.existsSync()) { print("Error: no template named ${argValues["template"]}"); - return -1; + return 1; } print(""); @@ -85,6 +85,8 @@ class CLITemplateCreator extends CLICommand { print("Generating project files..."); await createProjectSpecificFiles(destDirectory.path, aqueductVersion); + await replaceAqueductDependencyString(destDirectory.path, aqueductVersion); + print("Fetching project dependencies..."); Process.runSync("pub", ["get", "--no-packages-dir"], workingDirectory: destDirectory.path, runInShell: true); @@ -232,6 +234,17 @@ class CLITemplateCreator extends CLICommand { .copySync(new File(path_lib.join(directoryPath, "config.yaml")).path); } + Future replaceAqueductDependencyString( + String destDirectoryPath, String aqueductVersion) async { + var pubspecFile = + new File(path_lib.join(destDirectoryPath, "pubspec.yaml")); + var contents = pubspecFile.readAsStringSync(); + + contents = contents.replaceFirst("aqueduct: \"^1.0.0\"", aqueductVersion); + + pubspecFile.writeAsStringSync(contents); + } + void copyProjectFiles(Directory destinationDirectory, Directory sourceDirectory, String projectName) { try { diff --git a/lib/src/db/managed/data_model.dart b/lib/src/db/managed/data_model.dart index 89c1b6254..262febc1a 100644 --- a/lib/src/db/managed/data_model.dart +++ b/lib/src/db/managed/data_model.dart @@ -11,7 +11,7 @@ import 'data_model_builder.dart'; /// types that extend [ManagedObject]. class ManagedDataModel { /// Creates an instance of [ManagedDataModel] from a list of types that extend [ManagedObject]. It is preferable - /// to use [ManagedDataModel.fromPackageContainingType] over this method. + /// to use [ManagedDataModel.fromCurrentMirrorSystem] over this method. /// /// To register a class as a managed object within this data model, you must include its type in the list. Example: /// @@ -22,30 +22,49 @@ class ManagedDataModel { _persistentTypeToEntityMap = builder.persistentTypeToEntityMap; } - /// Creates an instance on [ManagedDataModel] from all of the declared [ManagedObject] subclasses declared in the same package as [type]. + /// Creates an instance of a [ManagedDataModel] from all subclasses of [ManagedObject] in all libraries visible to the calling library. + /// + /// This constructor will search every available package and file library that is visible to the library + /// that runs this constructor for subclasses of [ManagedObject]. A [ManagedEntity] will be created + /// and stored in this instance for every such class found. /// - /// This is a convenience constructor for creating a [ManagedDataModel] from an application package. It will find all subclasses of [ManagedObject] - /// in the package that [type] belongs to. Typically, you pass the [Type] of an application's [RequestSink] subclass. - ManagedDataModel.fromPackageContainingType(Type type) { - LibraryMirror libMirror = reflectType(type).owner; + /// Standard Dart libraries (prefixed with 'dart:') and URL-encoded libraries (prefixed with 'data:') are not searched. + /// + /// This is the preferred method of instantiating this type. + ManagedDataModel.fromCurrentMirrorSystem() { + var managedObjectMirror = reflectClass(ManagedObject); + var classes = currentMirrorSystem() + .libraries + .values + .where((lib) => lib.uri.scheme == "package" || lib.uri.scheme == "file") + .expand((lib) => lib.declarations.values) + .where((decl) => + decl is ClassMirror && + decl.isSubclassOf(managedObjectMirror) && + decl != managedObjectMirror) + .map((decl) => decl as ClassMirror) + .toList(); - var builder = - new DataModelBuilder(this, _modelTypesFromLibraryMirror(libMirror)); + var builder = new DataModelBuilder( + this, classes.map((cm) => cm.reflectedType).toList()); _entities = builder.entities; _persistentTypeToEntityMap = builder.persistentTypeToEntityMap; } + /// Creates an instance on [ManagedDataModel] from all of the declared [ManagedObject] subclasses declared in the same package as [type]. + /// + /// This method now simply calls [ManagedDataModel.fromCurrentMirrorSystem]. + @deprecated + factory ManagedDataModel.fromPackageContainingType(Type type) { + return new ManagedDataModel.fromCurrentMirrorSystem(); + } + /// Creates an instance of a [ManagedDataModel] from a package on the filesystem. /// - /// This method is used by database migration tools. - ManagedDataModel.fromURI(Uri libraryURI) { - if (!libraryURI.isAbsolute) { - libraryURI = new Uri.file(libraryURI.path); - } - var libMirror = currentMirrorSystem().libraries[libraryURI]; - var builder = - new DataModelBuilder(this, _modelTypesFromLibraryMirror(libMirror)); - _entities = builder.entities; + /// This method now simply calls [ManagedDataModel.fromCurrentMirrorSystem]. + @deprecated + factory ManagedDataModel.fromURI(Uri libraryURI) { + return new ManagedDataModel.fromCurrentMirrorSystem(); } Iterable get entities => _entities.values; @@ -65,25 +84,6 @@ class ManagedDataModel { ManagedEntity entityForType(Type type) { return _entities[type] ?? _persistentTypeToEntityMap[type]; } - - List _modelTypesFromLibraryMirror(LibraryMirror libMirror) { - var allLibraries = libMirror.libraryDependencies - .where((dep) => dep.isExport) - .map((dep) => dep.targetLibrary) - .toList(); - allLibraries.add(libMirror); - - var modelMirror = reflectClass(ManagedObject); - Iterable allClasses = allLibraries - .expand((lm) => lm.declarations.values) - .where((decl) => decl is ClassMirror) - .map((decl) => decl as ClassMirror); - - return allClasses - .where((m) => m.isSubclassOf(modelMirror)) - .map((m) => m.reflectedType) - .toList(); - } } /// Thrown when a [ManagedDataModel] encounters an error. diff --git a/lib/src/db/schema/migration.dart b/lib/src/db/schema/migration.dart index 50ef25bdc..dcd27045f 100644 --- a/lib/src/db/schema/migration.dart +++ b/lib/src/db/schema/migration.dart @@ -111,8 +111,7 @@ class MigrationExecutor { var generator = new SourceGenerator( (List args, Map values) async { - var dataModel = new ManagedDataModel.fromURI( - new Uri(scheme: "package", path: args[0])); + var dataModel = new ManagedDataModel.fromCurrentMirrorSystem(); var schema = new Schema.fromDataModel(dataModel); return schema.asMap(); @@ -164,8 +163,7 @@ class MigrationExecutor { var generator = new SourceGenerator( (List args, Map values) async { - var dataModel = new ManagedDataModel.fromURI( - new Uri(scheme: "package", path: args[0])); + var dataModel = new ManagedDataModel.fromCurrentMirrorSystem(); var schema = new Schema.fromDataModel(dataModel); return SchemaBuilder.sourceForSchemaUpgrade( diff --git a/test/auth/auth_controller_test.dart b/test/auth/auth_controller_test.dart index f5d034c27..9ba1e626d 100644 --- a/test/auth/auth_controller_test.dart +++ b/test/auth/auth_controller_test.dart @@ -14,7 +14,8 @@ void main() { Router router; setUpAll(() { - authenticationServer = new AuthServer(new AuthDelegate(context)); + authenticationServer = + new AuthServer(new AuthDelegate(context)); router = new Router(); router diff --git a/test/base/controller_test.dart b/test/base/controller_test.dart index 04fcc42a4..fe9816340 100644 --- a/test/base/controller_test.dart +++ b/test/base/controller_test.dart @@ -13,8 +13,8 @@ void main() { HttpServer server; setUpAll(() { - new ManagedContext(new ManagedDataModel([TestModel]), - new DefaultPersistentStore()); + new ManagedContext( + new ManagedDataModel([TestModel]), new DefaultPersistentStore()); }); tearDown(() async { diff --git a/test/command/create_test.dart b/test/command/create_test.dart new file mode 100644 index 000000000..4682d5cab --- /dev/null +++ b/test/command/create_test.dart @@ -0,0 +1,110 @@ +import 'package:test/test.dart'; +import 'dart:io'; +import 'package:path/path.dart' as path_lib; + +Directory testTemplateDirectory = new Directory("tmp_templates"); + +void main() { + setUp(() { + testTemplateDirectory.createSync(); + }); + + tearDown(() { + testTemplateDirectory.deleteSync(recursive: true); + }); + + group("Project naming", () { + test("Appropriately named project gets created correctly", () { + var res = runWith(["-n", "test_project"]); + expect(res.exitCode, 0); + + expect( + new Directory( + path_lib.join(testTemplateDirectory.path, "test_project")) + .existsSync(), + true); + }); + + test("Project name with bad characters fails immediately", () { + var res = runWith(["-n", "!@"]); + expect(res.exitCode, 1); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + + test("Project name with uppercase characters fails immediately", () { + var res = runWith(["-n", "ANeatApp"]); + expect(res.exitCode, 1); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + + test("Project name with dashes fails immediately", () { + var res = runWith(["-n", "a-neat-app"]); + expect(res.exitCode, 1); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + + test("Not providing name returns error", () { + var res = runWith([]); + expect(res.exitCode, 1); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + + test("Providing empty name returns error", () { + var res = runWith(["-n"]); + expect(res.exitCode, 255); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + }); + + group("Templates from path", () { + test("Template gets generated from local path, project points to it", () { + var res = runWith(["-n", "test_project"]); + expect(res.exitCode, 0); + + var aqueductLocationString = + new File(projectPath("test_project", file: ".packages")) + .readAsStringSync() + .split("\n") + .firstWhere((p) => p.startsWith("aqueduct:")) + .split("aqueduct:") + .last; + + var path = path_lib.normalize(path_lib.fromUri(aqueductLocationString)); + expect(path, path_lib.join(Directory.current.path, "lib")); + }); + + test("Tests run on template generated from local path", () { + var res = runWith(["-n", "test_project"]); + expect(res.exitCode, 0); + + res = Process.runSync("pub", ["run", "test", "-j", "1"], + runInShell: true, + workingDirectory: + path_lib.join(testTemplateDirectory.path, "test_project")); + expect(res.exitCode, 0); + expect(res.stdout, contains("All tests passed")); + }); + }); +} + +ProcessResult runWith(List args) { + var aqueductDirectory = Directory.current.path; + var result = Process.runSync( + "pub", ["global", "activate", "-spath", "$aqueductDirectory"], + runInShell: true); + expect(result.exitCode, 0); + + var allArgs = ["create", "--path-source", "$aqueductDirectory"]; + allArgs.addAll(args); + return Process.runSync("aqueduct", allArgs, + runInShell: true, workingDirectory: testTemplateDirectory.path); +} + +String projectPath(String projectName, {String file}) { + return path_lib.join(testTemplateDirectory.path, projectName, file); +} diff --git a/test/db/model_test.dart b/test/db/model_test.dart index 818bac545..be17a48c5 100644 --- a/test/db/model_test.dart +++ b/test/db/model_test.dart @@ -7,7 +7,7 @@ void main() { setUpAll(() { var ps = new DefaultPersistentStore(); ManagedDataModel dm = - new ManagedDataModel([TransientTest, TransientTypeTest, User, Post]); + new ManagedDataModel([TransientTest, TransientTypeTest, User, Post]); var _ = new ManagedContext(dm, ps); }); diff --git a/test/db/postgresql/adapter_test.dart b/test/db/postgresql/adapter_test.dart index b046e5523..c969a88f7 100644 --- a/test/db/postgresql/adapter_test.dart +++ b/test/db/postgresql/adapter_test.dart @@ -4,12 +4,14 @@ import 'package:postgres/postgres.dart'; import 'dart:async'; void main() { - PostgreSQLPersistentStore persistentStore = new PostgreSQLPersistentStore(() async { + PostgreSQLPersistentStore persistentStore = + new PostgreSQLPersistentStore(() async { var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); await connection.open(); return connection; - });; + }); + ; tearDown(() async { await persistentStore.close(); diff --git a/test/test_project/lib/src/wildfire_sink.dart b/test/test_project/lib/src/wildfire_sink.dart index 92ab9091e..e801e067b 100644 --- a/test/test_project/lib/src/wildfire_sink.dart +++ b/test/test_project/lib/src/wildfire_sink.dart @@ -58,8 +58,7 @@ class WildfireSink extends RequestSink { ManagedContext contextWithConnectionInfo( DatabaseConnectionConfiguration database) { var connectionInfo = configuration.database; - var dataModel = - new ManagedDataModel.fromPackageContainingType(this.runtimeType); + var dataModel = new ManagedDataModel.fromCurrentMirrorSystem(); var psc = new PostgreSQLPersistentStore.fromConnectionInfo( connectionInfo.username, connectionInfo.password, diff --git a/test/utilities/test_client_test.dart b/test/utilities/test_client_test.dart index a534f9303..d2d649342 100644 --- a/test/utilities/test_client_test.dart +++ b/test/utilities/test_client_test.dart @@ -515,7 +515,7 @@ void main() { test("Partial match, null and not present", () async { var defaultTestClient = new TestClient.onPort(4000); - server.queueResponse(new Response.ok({"foo": null, "bar" : "boo"})); + server.queueResponse(new Response.ok({"foo": null, "bar": "boo"})); var response = await defaultTestClient.request("/foo").get(); expect(response, hasBody(partial({"bar": "boo"}))); expect(response, hasBody(partial({"foo": isNull}))); @@ -525,8 +525,10 @@ void main() { expect(response, hasBody(partial({"foo": isNotPresent}))); expect(true, false); } on TestFailure catch (e) { - expect(e.toString(), - contains("Expected: Body: Partially matches: {foo: ,}")); + expect( + e.toString(), + contains( + "Expected: Body: Partially matches: {foo: ,}")); expect(e.toString(), contains('Body: {"foo":null,"bar":"boo"}')); } try { From 54aec02085162c773ffa440fc5bff3939ab5df46 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 6 Dec 2016 14:19:43 -0500 Subject: [PATCH 30/36] Jc/explicit query values (#148) * Tests and fixes for template generation and data model generation * Fixed some exit code stuff * dartfmt * Removed unused imports * Disallow multiple query parameters of same key if arg type is not List in HTTPController, allow when is * dartfmt * update changelog * Improve willSendResponse and tests --- CHANGELOG.md | 1 + lib/src/http/http_controller.dart | 11 +- lib/src/http/http_controller_internal.dart | 5 + lib/src/http/request_controller.dart | 128 ++++++++++++--------- test/base/controller_test.dart | 57 +++++++++ test/base/request_controller_test.dart | 91 ++++++++++++++- test/command/create_test.dart | 1 + 7 files changed, 227 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dda9a7df1..6d69f111f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - BREAKING CHANGE: Added new `Response.contentType` property. Adding "Content-Type" to the headers of a `Response` no longer has any effect; use this property instead. - `ManagedDataModel`s now scan all libraries for `ManagedObject` subclasses to generate a data model. Use `ManagedDataModel.fromCurrentMirrorSystem` to create instances of `ManagedDataModel`. - The *last* instantiated `ManagedContext` now becomes the `ManagedContext.defaultContext`; prior to this change, it was the first instantiated context. Added `ManagedContext.standalone` to opt out of setting the default context. +- @HTTPQuery parameters in HTTPController responder method will now only allow multiple keys in the query string if and only if the argument type is a List. ## 1.0.3 - Fix to allow Windows user to use `aqueduct setup`. diff --git a/lib/src/http/http_controller.dart b/lib/src/http/http_controller.dart index 677551871..15f5178a9 100644 --- a/lib/src/http/http_controller.dart +++ b/lib/src/http/http_controller.dart @@ -79,12 +79,6 @@ abstract class HTTPController extends RequestController { /// handled by the appropriate responder method. void didDecodeRequestBody(dynamic decodedObject) {} - /// Executed prior to [Response] being sent, but after the responder method has been executed. - /// - /// This method is used to post-process a response before it is finally sent. By default, does nothing. - /// This method will have no impact on when or how the [Response] is sent, is is simply informative. - void willSendResponse(Response response) {} - bool _requestContentTypeIsSupported(Request req) { var incomingContentType = request.innerRequest.headers.contentType; return acceptedContentTypes.firstWhere((ct) { @@ -159,8 +153,6 @@ abstract class HTTPController extends RequestController { response.contentType = responseContentType; } - willSendResponse(response); - return response; } @@ -182,7 +174,8 @@ abstract class HTTPController extends RequestController { return response; } on InternalControllerException catch (e) { - return e.response; + var response = e.response; + return response; } } diff --git a/lib/src/http/http_controller_internal.dart b/lib/src/http/http_controller_internal.dart index 071b2919f..57feee6a2 100644 --- a/lib/src/http/http_controller_internal.dart +++ b/lib/src/http/http_controller_internal.dart @@ -247,6 +247,11 @@ dynamic convertParameterListWithMirror( convertParameterWithMirror(str, typeMirror.typeArguments.first)) .toList(); } else { + if (parameterValues.length > 1) { + throw new InternalControllerException( + "Duplicate value for parameter", HttpStatus.BAD_REQUEST, + responseMessage: "Duplicate parameter for non-List parameter type"); + } return convertParameterWithMirror(parameterValues.first, typeMirror); } } diff --git a/lib/src/http/request_controller.dart b/lib/src/http/request_controller.dart index 675faa390..08a69822a 100644 --- a/lib/src/http/request_controller.dart +++ b/lib/src/http/request_controller.dart @@ -140,16 +140,19 @@ class RequestController extends Object with APIDocumentable { if (policy != null) { if (!policy.validatePreflightRequest(req.innerRequest)) { - req.respond(new Response.forbidden()); + var response = new Response.forbidden(); + _sendResponse(req, response); logger.info(req.toDebugString(includeHeaders: true)); } else { - req.respond(policy.preflightResponse(req)); + var response = policy.preflightResponse(req); + _sendResponse(req, response); logger.info(req.toDebugString()); } return; } else { // If we don't have a policy, then a preflight request makes no sense. - req.respond(new Response.forbidden()); + var response = new Response.forbidden(); + _sendResponse(req, response); logger.info(req.toDebugString(includeHeaders: true)); return; } @@ -160,13 +163,14 @@ class RequestController extends Object with APIDocumentable { if (result is Request && nextController != null) { nextController.receive(req); } else if (result is Response) { - applyCORSHeadersIfNecessary(req, result); - req.respond(result); + _sendResponse(req, result, includeCORSHeaders: true); logger.info(req.toDebugString()); } } catch (any, stacktrace) { - _handleError(req, any, stacktrace); + try { + _handleError(req, any, stacktrace); + } catch (_) {} } } @@ -186,62 +190,72 @@ class RequestController extends Object with APIDocumentable { return req; } + /// Executed prior to [Response] being sent. + /// + /// This method is used to post-process [response] just before it is sent. By default, does nothing. + /// The [response] may be altered prior to being sent. This method will be executed for all requests, + /// including server errors. + void willSendResponse(Response response) {} + + void _sendResponse(Request request, Response response, {bool includeCORSHeaders: false}) { + if (includeCORSHeaders) { + applyCORSHeadersIfNecessary(request, response); + } + willSendResponse(response); + request.respond(response); + } + void _handleError(Request request, dynamic caughtValue, StackTrace trace) { - try { - if (caughtValue is HTTPResponseException) { - var response = caughtValue.response; - applyCORSHeadersIfNecessary(request, response); - request.respond(response); - - logger.info( - "${request.toDebugString(includeHeaders: true, includeBody: true)}"); - } else if (caughtValue is QueryException && - caughtValue.event != QueryExceptionEvent.internalFailure) { - // Note that if the event is an internal failure, this code is skipped and the 500 handler is executed. - var statusCode = 500; - switch (caughtValue.event) { - case QueryExceptionEvent.requestFailure: - statusCode = 400; - break; - case QueryExceptionEvent.internalFailure: - statusCode = 500; - break; - case QueryExceptionEvent.connectionFailure: - statusCode = 503; - break; - case QueryExceptionEvent.conflict: - statusCode = 409; - break; - } + if (caughtValue is HTTPResponseException) { + var response = caughtValue.response; + _sendResponse(request, response, includeCORSHeaders: true); + + logger.info( + "${request.toDebugString(includeHeaders: true, includeBody: true)}"); + } else if (caughtValue is QueryException && + caughtValue.event != QueryExceptionEvent.internalFailure) { + // Note that if the event is an internal failure, this code is skipped and the 500 handler is executed. + var statusCode = 500; + switch (caughtValue.event) { + case QueryExceptionEvent.requestFailure: + statusCode = 400; + break; + case QueryExceptionEvent.internalFailure: + statusCode = 500; + break; + case QueryExceptionEvent.connectionFailure: + statusCode = 503; + break; + case QueryExceptionEvent.conflict: + statusCode = 409; + break; + } - var response = - new Response(statusCode, null, {"error": caughtValue.toString()}); - applyCORSHeadersIfNecessary(request, response); - request.respond(response); - - logger.info( - "${request.toDebugString(includeHeaders: true, includeBody: true)}"); - } else { - var body = null; - if (includeErrorDetailsInServerErrorResponses) { - body = { - "error": "${this.runtimeType}: $caughtValue.", - "stacktrace": trace.toString() - }; - } + var response = + new Response(statusCode, null, {"error": caughtValue.toString()}); + _sendResponse(request, response, includeCORSHeaders: true); + + logger.info( + "${request.toDebugString(includeHeaders: true, includeBody: true)}"); + } else { + var body = null; + if (includeErrorDetailsInServerErrorResponses) { + body = { + "error": "${this.runtimeType}: $caughtValue.", + "stacktrace": trace.toString() + }; + } - var response = new Response.serverError(body: body) - ..contentType = ContentType.JSON; + var response = new Response.serverError(body: body) + ..contentType = ContentType.JSON; - applyCORSHeadersIfNecessary(request, response); - request.respond(response); + _sendResponse(request, response, includeCORSHeaders: true); - logger.severe( - "${request.toDebugString(includeHeaders: true, includeBody: true)}", - caughtValue, - trace); - } - } catch (_) {} + logger.severe( + "${request.toDebugString(includeHeaders: true, includeBody: true)}", + caughtValue, + trace); + } } RequestController _lastRequestController() { diff --git a/test/base/controller_test.dart b/test/base/controller_test.dart index fe9816340..b217886f3 100644 --- a/test/base/controller_test.dart +++ b/test/base/controller_test.dart @@ -431,6 +431,55 @@ void main() { expect(JSON.decode(resp.body)["error"], contains("Table")); expect(JSON.decode(resp.body)["error"], contains("Shaqs")); }); + + test("May only be one query parameter if arg type is not List", + () async { + server = await enableController("/a", DuplicateParamController); + var resp = await http + .get("http://localhost:4040/a?list=a&list=b&single=x&single=y"); + + expect(resp.statusCode, 400); + + expect(JSON.decode(resp.body)["error"], + "Duplicate parameter for non-List parameter type"); + }); + + test("Can be more than one query parameters for arg type that is List", + () async { + server = await enableController("/a", DuplicateParamController); + var resp = + await http.get("http://localhost:4040/a?list=a&list=b&single=x"); + + expect(resp.statusCode, 200); + + expect(JSON.decode(resp.body), { + "list": ["a", "b"], + "single": "x" + }); + }); + + test("Can be exactly one query parameter for arg type that is List", + () async { + server = await enableController("/a", DuplicateParamController); + var resp = await http.get("http://localhost:4040/a?list=a&single=x"); + + expect(resp.statusCode, 200); + + expect(JSON.decode(resp.body), { + "list": ["a"], + "single": "x" + }); + }); + + test("Missing required List query parameter still returns 400", + () async { + server = await enableController("/a", DuplicateParamController); + var resp = await http.get("http://localhost:4040/a?single=x"); + + expect(resp.statusCode, 400); + + expect(JSON.decode(resp.body)["error"], contains("list")); + }); }); } @@ -621,6 +670,14 @@ class ContentTypeController extends HTTPController { } } +class DuplicateParamController extends HTTPController { + @httpGet + Future getThing(@HTTPQuery("list") List list, + @HTTPQuery("single") String single) async { + return new Response.ok({"list": list, "single": single}); + } +} + Future enableController(String pattern, Type controller) async { var router = new Router(); router.route(pattern).generate( diff --git a/test/base/request_controller_test.dart b/test/base/request_controller_test.dart index 910479211..c65849808 100644 --- a/test/base/request_controller_test.dart +++ b/test/base/request_controller_test.dart @@ -37,7 +37,7 @@ void main() { await next.receive(req); - // We'll get here only if delivery succeeds, evne tho the response must be an error + // We'll get here only if delivery succeeds, even tho the response must be an error ensureExceptionIsCapturedByDeliver.complete(true); }); @@ -295,6 +295,67 @@ void main() { var resp = await http.get("http://localhost:8080"); expect(resp.statusCode, 500); }); + + test("willSendResponse is always called prior to Response being sent for preflight requests", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.generate(() => new Always200Controller()); + await next.receive(req); + }); + + // Invalid preflight + var req = await (new HttpClient().open("OPTIONS", "localhost", 8080, "")); + req.headers.set("Origin", "http://foobar.com"); + req.headers.set("Access-Control-Request-Method", "POST"); + req.headers + .set("Access-Control-Request-Headers", "accept, authorization"); + var resp = await req.close(); + + expect(resp.statusCode, 200); + expect(JSON.decode((new String.fromCharCodes(await resp.first))), {"statusCode" : 403}); + + // valid preflight + req = await (new HttpClient().open("OPTIONS", "localhost", 8080, "")); + req.headers.set("Origin", "http://somewhere.com"); + req.headers.set("Access-Control-Request-Method", "POST"); + req.headers + .set("Access-Control-Request-Headers", "accept, authorization"); + resp = await req.close(); + + expect(resp.statusCode, 200); + expect(resp.headers.value("access-control-allow-methods"), "POST, PUT, DELETE, GET"); + expect(JSON.decode((new String.fromCharCodes(await resp.first))), {"statusCode" : 200}); + }); + + test("willSendResponse is always called prior to Response being sent for normal requests", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.generate(() => new Always200Controller()); + await next.receive(req); + }); + + // normal response + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(JSON.decode(resp.body), {"statusCode" : 100}); + + // httpresponseexception + resp = await http.get("http://localhost:8080?q=http_response_exception"); + expect(resp.statusCode, 200); + expect(JSON.decode(resp.body), {"statusCode" : 400}); + + // query exception + resp = await http.get("http://localhost:8080?q=query_exception"); + expect(resp.statusCode, 200); + expect(JSON.decode(resp.body), {"statusCode" : 503}); + + // any other exception (500) + resp = await http.get("http://localhost:8080?q=server_error"); + expect(resp.statusCode, 200); + expect(JSON.decode(resp.body), {"statusCode" : 500}); + }); } class SomeObject implements HTTPSerializable { @@ -304,3 +365,31 @@ class SomeObject implements HTTPSerializable { return {"name": name}; } } + +class Always200Controller extends RequestController { + Always200Controller() { + policy.allowedOrigins = ["http://somewhere.com"]; + } + @override + Future processRequest(Request req) async { + var q = req.innerRequest.uri.queryParameters["q"]; + if (q == "http_response_exception") { + throw new HTTPResponseException(400, "ok"); + } else if (q == "query_exception") { + throw new QueryException(QueryExceptionEvent.connectionFailure); + } else if (q == "server_error") { + throw new FormatException("whocares"); + } + return new Response(100, null, null); + } + + @override + void willSendResponse(Response resp) { + var originalMap = { + "statusCode" : resp.statusCode + }; + resp.statusCode = 200; + resp.body = originalMap; + resp.contentType = ContentType.JSON; + } +} \ No newline at end of file diff --git a/test/command/create_test.dart b/test/command/create_test.dart index 4682d5cab..648ca9fc4 100644 --- a/test/command/create_test.dart +++ b/test/command/create_test.dart @@ -101,6 +101,7 @@ ProcessResult runWith(List args) { var allArgs = ["create", "--path-source", "$aqueductDirectory"]; allArgs.addAll(args); + return Process.runSync("aqueduct", allArgs, runInShell: true, workingDirectory: testTemplateDirectory.path); } From 1bc0f7ea353c14e9607dc4bc7afeeed11dd18383 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 7 Dec 2016 12:50:45 -0500 Subject: [PATCH 31/36] Fixing some naming and ordering in template (#150) * Fixing some naming and ordering in template * Fixed test issue with 1.21 --- .../default/lib/controller/register_controller.dart | 9 ++++----- test/base/recovery_test.dart | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/example/templates/default/lib/controller/register_controller.dart b/example/templates/default/lib/controller/register_controller.dart index e73834726..52a4eaebe 100644 --- a/example/templates/default/lib/controller/register_controller.dart +++ b/example/templates/default/lib/controller/register_controller.dart @@ -3,10 +3,12 @@ import '../wildfire.dart'; class RegisterController extends QueryController { @httpPost createUser() async { - if (query.values.username == null || query.values.password == null) { + if (query.values.email == null || query.values.password == null) { return new Response.badRequest( - body: {"error": "Username and password required."}); + body: {"error": "email and password required."}); } + var credentials = AuthorizationBasicParser + .parse(request.innerRequest.headers.value(HttpHeaders.AUTHORIZATION)); var salt = AuthServer.generateRandomSalt(); var hashedPassword = @@ -15,9 +17,6 @@ class RegisterController extends QueryController { query.values.salt = salt; var u = await query.insert(); - - var credentials = AuthorizationBasicParser - .parse(request.innerRequest.headers.value(HttpHeaders.AUTHORIZATION)); var token = await request.authorization.grantingServer.authenticate( u.username, query.values.password, diff --git a/test/base/recovery_test.dart b/test/base/recovery_test.dart index b5d8735c4..c993a9d19 100644 --- a/test/base/recovery_test.dart +++ b/test/base/recovery_test.dart @@ -30,7 +30,7 @@ main() { var errorMessage = await app.logger.onRecord.first; expect(errorMessage.message, contains("Uncaught exception")); expect( - errorMessage.error.toString(), contains("method not found: 'foo'")); + errorMessage.error.toString(), contains("foo")); expect(errorMessage.stackTrace, isNotNull); // And then we should make sure everything is working just fine. @@ -51,7 +51,7 @@ main() { logMessages.forEach((errorMessage) { expect(errorMessage.message, contains("Uncaught exception")); expect( - errorMessage.error.toString(), contains("method not found: 'foo'")); + errorMessage.error.toString(), contains("foo")); expect(errorMessage.stackTrace, isNotNull); }); From 9dde4e91876f18d65cea3add4aa997b5fe64ca41 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 7 Dec 2016 13:27:10 -0500 Subject: [PATCH 32/36] Getting code coverage to only run on master (#151) * Getting code coverage to only run on master * Moving scripts into ci dir * Moving scripts into ci dir --- .travis.yml | 10 ++-------- ci/after_script.sh | 5 +++++ ci/script.sh | 7 +++++++ 3 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 ci/after_script.sh create mode 100644 ci/script.sh diff --git a/.travis.yml b/.travis.yml index 0fb321009..c62f38e91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,14 +10,8 @@ before_script: - psql -c "alter user dart with password 'dart';" -U postgres - psql -c 'grant all on database dart_test to dart;' -U postgres - pub get -script: - - pub run test -j 1 -r expanded - - pub global activate -sgit https://github.com/stablekernel/codecov_dart.git - - dart_codecov_generator --report-on=lib/ --verbose --no-html -after_success: - - if [ "$TRAVIS_BRANCH" == "master" ]; then - bash <(curl -s https://codecov.io/bash) - fi +script: sh ci/script.sh +after_success: sh ci/after_script.sh branches: only: - master diff --git a/ci/after_script.sh b/ci/after_script.sh new file mode 100644 index 000000000..9383dcab5 --- /dev/null +++ b/ci/after_script.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +if [ "$TRAVIS_BRANCH" == "master" ]; then + bash <(curl -s https://codecov.io/bash) +fi \ No newline at end of file diff --git a/ci/script.sh b/ci/script.sh new file mode 100644 index 000000000..fd9bf8de3 --- /dev/null +++ b/ci/script.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +pub run test -j 1 -r expanded +if [ "$TRAVIS_BRANCH" == "master" ]; then + pub global activate -sgit https://github.com/stablekernel/codecov_dart.git + dart_codecov_generator --report-on=lib/ --verbose --no-html +fi \ No newline at end of file From 8ab4bf279c47f4263fe47907dea5458e50e4a0bb Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 7 Dec 2016 14:07:22 -0500 Subject: [PATCH 33/36] Updating after_script --- ci/after_script.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ci/after_script.sh b/ci/after_script.sh index 9383dcab5..6858989af 100644 --- a/ci/after_script.sh +++ b/ci/after_script.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash if [ "$TRAVIS_BRANCH" == "master" ]; then - bash <(curl -s https://codecov.io/bash) + curl -s https://codecov.io/bash > .codecov + chmod +x .codecov + ./.codecov -f lcov.info -X xcode fi \ No newline at end of file From e8512959b4411e57cadcde2a6f98580fae88eeba Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 7 Dec 2016 14:19:55 -0500 Subject: [PATCH 34/36] Changing script error --- ci/after_script.sh | 4 ++-- ci/script.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ci/after_script.sh b/ci/after_script.sh index 6858989af..6bc465cce 100644 --- a/ci/after_script.sh +++ b/ci/after_script.sh @@ -1,6 +1,6 @@ -#!/usr/bin/env bash +#!/bin/bash -if [ "$TRAVIS_BRANCH" == "master" ]; then +if [[ "$TRAVIS_BRANCH" == "master" ]]; then curl -s https://codecov.io/bash > .codecov chmod +x .codecov ./.codecov -f lcov.info -X xcode diff --git a/ci/script.sh b/ci/script.sh index fd9bf8de3..4510e9924 100644 --- a/ci/script.sh +++ b/ci/script.sh @@ -1,7 +1,7 @@ -#!/usr/bin/env bash +#!/bin/bash pub run test -j 1 -r expanded -if [ "$TRAVIS_BRANCH" == "master" ]; then +if [[ "$TRAVIS_BRANCH" == "master" ]]; then pub global activate -sgit https://github.com/stablekernel/codecov_dart.git dart_codecov_generator --report-on=lib/ --verbose --no-html fi \ No newline at end of file From dae00899b6a39cf4b283a3cc645b82fa581fc0e0 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 7 Dec 2016 14:42:47 -0500 Subject: [PATCH 35/36] Debugging travis script --- .travis.yml | 4 ++-- ci/script.sh | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c62f38e91..6441344c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ before_script: - psql -c "alter user dart with password 'dart';" -U postgres - psql -c 'grant all on database dart_test to dart;' -U postgres - pub get -script: sh ci/script.sh -after_success: sh ci/after_script.sh +script: bash ci/script.sh +after_success: bash ci/after_script.sh branches: only: - master diff --git a/ci/script.sh b/ci/script.sh index 4510e9924..d34cb6b68 100644 --- a/ci/script.sh +++ b/ci/script.sh @@ -1,5 +1,9 @@ #!/bin/bash +if [[ "$TRAVIS_BRANCH" == "master" ]]; then + echo 'ok' +fi + pub run test -j 1 -r expanded if [[ "$TRAVIS_BRANCH" == "master" ]]; then pub global activate -sgit https://github.com/stablekernel/codecov_dart.git From 5e3cbf923e95ab89de37c1ad9f9f97d3603d5c71 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 7 Dec 2016 15:09:05 -0500 Subject: [PATCH 36/36] Getting reason why tests fail in code cov env, removing debug code from travisci script --- ci/script.sh | 4 ---- test/command/create_test.dart | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ci/script.sh b/ci/script.sh index d34cb6b68..4510e9924 100644 --- a/ci/script.sh +++ b/ci/script.sh @@ -1,9 +1,5 @@ #!/bin/bash -if [[ "$TRAVIS_BRANCH" == "master" ]]; then - echo 'ok' -fi - pub run test -j 1 -r expanded if [[ "$TRAVIS_BRANCH" == "master" ]]; then pub global activate -sgit https://github.com/stablekernel/codecov_dart.git diff --git a/test/command/create_test.dart b/test/command/create_test.dart index 648ca9fc4..030947cf0 100644 --- a/test/command/create_test.dart +++ b/test/command/create_test.dart @@ -86,6 +86,7 @@ void main() { runInShell: true, workingDirectory: path_lib.join(testTemplateDirectory.path, "test_project")); + print("${res.stdout} ${res.stderr}"); expect(res.exitCode, 0); expect(res.stdout, contains("All tests passed")); });