Aqueduct 3.2.0 is now available on pub. This release adds better support for ingesting request bodies into objects. See the changelog for a complete list of changes.
Adding Serializable.read
In previous versions of Aqueduct, serializable objects (such as ManagedObject
) implemented and invoked readFromMap
to ingest key-value pairs into a structured object. In Aqueduct 3.2 and later, serializable objects will still implement readFromMap
to determine how data is ingested into an object, but you will now invoke read
when performing the ingestion. The read
method adds additional behavior for filtering a request body to ignore, reject or require certain keys.
@Operation.post()
Future<Response> createPerson() async {
final person = Person()..read(await request.body.decode(),
ignore: ["id"], reject: ["password"], require: ["age", "height"]);
}
Filters are executed after the body is decoded into a map, but before Serializable.readFromMap
is run. Keys that marked as ignore
as removed from the request body map prior to invoking the object’s readFromMap
. If a key marked as reject
is located in the map or a key marked as require
is not in the map, a 400 Bad Request exception is thrown.
This new behavior allows your types to have global behavior for ingesting all of its properties and granular control at the endpoint level. For example, Person.readFromMap
knows how to read the key id
and assign it to its id
property, but for this particular endpoint, we strip that key away before readFromMap
is called.
You can also use filters when binding Serializable
or List<Serializable>
objects.
@Operation.post()
Future<Response> createPerson(
@Bind.body(ignore: ["id"]) Person person) async {
...
}
You can still invoke readFromMap
on serializable objects, but read
is now preferred.
Binding Primitive Types with @Bind.body
You can now bind primitive types – such as Map<String, dynamic>
and List<int>
– to the body of a request.
@Operation.post()
Future<Response> createPerson(
@Bind.body() Map<String, dynamic> map) async {
...
}
Note that you cannot use filters when binding primitives.
Validators on Relationships
Validate
annotations can now be added to belongs-to relationship properties. The validator will be run on the foreign key value only. For example, consider the following managed object:
class Child extends ManagedObject<_Child> implements _Child {}
class _Child {
@primaryKey
int id;
String name;
@Validate.compare(greaterThan: 1)
@Relate(#children)
Parent parent;
}
This validator will ensure that the key parent.id
is greater than 1 when the input JSON is. For example, the following JSON object when validated as a Child
will fail because parent.id
is not greater than 1:
{
"name": "Fred",
"parent": {
"id": 0
}
}
Validations that would normally run on a Parent
object are not run in this context. That is, if Parent.id
has validators, they are not run when ingesting a Child
object’s parent
property.
Change to @primaryKey
The @primaryKey
column annotation now automatically adds a Validate.constant
validator to the property. The constant validator prevents a Query
from updating the property it is associated with. In other words, if a client application sends the primary key of an object you will get a 400 Bad Request response by default when using @primaryKey
. This behavior prevents a controller from naively accepting changes to an object’s primary key.
If this causes a breaking change in your client-server interaction, you have two options. The preferred option is to update calls to readFromMap
to use read
and ignore the primary key – thus stripping away the violating key-value pair before the validator encounters it. The other option is to replace @primaryKey
annotations with the following:
Column(primaryKey: true, databaseType: ManagedPropertyType.bigInteger, autoincrement: true)