From 5170deb6e817e5b757b96be45bf62ef745d2b5bd Mon Sep 17 00:00:00 2001 From: Tim Condon <0xTim@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:08:15 +0000 Subject: [PATCH] Add note about DTO for parent relationship (#965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fluent/model.md | 12 +++++++----- docs/fluent/relations.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/docs/fluent/model.md b/docs/fluent/model.md index 8a9e34b62..76f128eed 100644 --- a/docs/fluent/model.md +++ b/docs/fluent/model.md @@ -359,12 +359,14 @@ app.get("planets") { req async throws in When serializing to / from `Codable`, model properties will use their variable names instead of keys. Relations will serialize as nested structures and any eager loaded data will be included. +!!! info + We recommend that for almost all cases you use a DTO instead of a model for your API responses and request bodies. See [Data Transfer Object](#data-transfer-object) for more information. + ### Data Transfer Object -Model's default `Codable` conformance can make simple usage and prototyping easier. However, it is not suitable for every use case. For certain situations you will need to use a data transfer object (DTO). +Model's default `Codable` conformance can make simple usage and prototyping easier. However, it exposes the underlying database information to the API. This is usually not desirable from both a security standpoint - returning sensitive fields such as a user's password hash is a bad idea - and a usability point of view. It makes it difficult to change the database schema without breaking the API, accept or return data in a different format, or to add or remove fields from the API. -!!! tip - A DTO is a separate `Codable` type representing the data structure you would like to encode or decode. +For most cases you use use a DTO, or data transfer object instead of a model (this is also known as a domain transfer object). A DTO is a separate `Codable` type representing the data structure you would like to encode or decode. These decouple your API from your database schema and allow you to make changes to your models without breaking your app's public API, have different versions and make your API nicer to use for your clients. Assume the following `User` model in the upcoming examples. @@ -434,9 +436,9 @@ app.get("users") { req async throws -> [GetUser] in } ``` -Even if the DTO's structure is identical to model's `Codable` conformance, having it as a separate type can help keep large projects tidy. If you ever need to make a change to your models properties, you don't have to worry about breaking your app's public API. You may also consider putting your DTOs in a separate package that can be shared with consumers of your API. +Another common use case is when dealing with relations, such as parent relations or children relations. See [the Parent documentation](relations.md##encoding-and-decoding-of-parents) for an example of how to use a DTO to make it easy to decode a model with a `@Parent` relation. -For these reasons, we highly recommend using DTOs wherever possible, especially for large projects. +Even if the DTO's structure is identical to model's `Codable` conformance, having it as a separate type can help keep large projects tidy. If you ever need to make a change to your models properties, you don't have to worry about breaking your app's public API. You may also consider putting your DTOs in a separate package that can be shared with consumers of your API and adding `Content` conformance in your Vapor app. ## Alias diff --git a/docs/fluent/relations.md b/docs/fluent/relations.md index de5418310..0d53c832d 100644 --- a/docs/fluent/relations.md +++ b/docs/fluent/relations.md @@ -61,6 +61,39 @@ The field definition is similar to `@Parent`'s except that the `.required` const .field("star_id", .uuid, .references("star", "id")) ``` +### Encoding and Decoding of Parents + +One thing to watch out for when working with `@Parent` relations is the way that you send and receive them. For example, in JSON, a `@Parent` for a `Planet` model might look like this: + +```json +{ + "id": "A616B398-A963-4EC7-9D1D-B1AA8A6F1107", + "star": { + "id": "A1B2C3D4-1234-5678-90AB-CDEF12345678" + } +} +``` + +Note how the `star` property is an object rather than the ID that you might expect. When sending the model as an HTTP body, it needs to match this for decoding to work. For this reason, we strongly recommend using a DTO to represent the model when sending it over the network. For example: + +```swift +struct PlanetDTO: Content { + var id: UUID? + var name: String + var star: Star.IDValue +} +``` + +Then you can decode the DTO and convert it into a model: + +```swift +let planetData = try req.content.decode(PlanetDTO.self) +let planet = Planet(id: planetData.id, name: planetData.name, starID: planetData.star) +try await planet.create(on: req.db) +``` + +The same applies when returning the model to clients. Your clients either need to be able to handle the nested structure, or you need to convert the model into a DTO before returning it. For more information about DTOs, see the [Model documentation](model.md#data-transfer-object) + ## Optional Child The `@OptionalChild` property creates a one-to-one relation between the two models. It does not store any values on the root model.