Aqueduct 3.2 is Now Available

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)
0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *