Skip to content

Commit

Permalink
Docs update for new API (#29)
Browse files Browse the repository at this point in the history
* updated readme

* updated CRUD for resource models docs

* updated docs: CRUDs for relations

* CRUDs for related resource model: added collapsing sections for deprecated APIs

* updated controller middleware docs

* updated deprecation notes

* Added migration guide from 1.0 to 2.0

* tests clean up

* tests clean up

* docs updates

* minor API changes

* docs updated

* minor changes in tests to be used as examples

* updated docs and readme

* updated readme
  • Loading branch information
KazaiMazai authored Nov 21, 2021
1 parent 534a862 commit e4fed30
Show file tree
Hide file tree
Showing 36 changed files with 1,986 additions and 612 deletions.
File renamed without changes.
334 changes: 178 additions & 156 deletions Docs/CRUD-Related-Resource-Models.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,211 +4,233 @@

#### How to create nested CRUD API with related models

### Siblings

1. Define Inputs, Outputs as usual
2. Define relation controller providing sibling relation keyPath and some *relationName* or nil, if not needed.

2. Define related resource controller providing relation keyPath:

```swift
let controller = Tag.Output
.controller(eagerLoading: EagerLoadingUnsupported.self)
.related(with: \Todo.$tags, relationName: "mentioned")
.create(using: Tag.CreateInput.self)
.read()
.update(using: Tag.UpdateInput.self)
.patch(using: Tag.PatchInput.self)
.collection(sorting: DefaultSorting.self,
filtering: DefaultFiltering.self)

```

3. Add related tags controller on top of "todos" route group:

struct StarForGalaxyNestedController {
func create(req: Request) throws -> EventLoopFuture<Star.Output> {
try RelatedResourceController<Star.Output>().create(
req: req,
using: Star.Input.self,
relationKeyPath: \Galaxy.$stars)
}

func read(req: Request) throws -> EventLoopFuture<Star.Output> {
try RelatedResourceController<Star.Output>().read(
req: req,
relationKeyPath: \Galaxy.$stars)
}

func update(req: Request) throws -> EventLoopFuture<Star.Output> {
try RelatedResourceController<Star.Output>().update(
req: req,
using: Star.Input.self,
relationKeyPath: \Galaxy.$stars)
}

func delete(req: Request) throws -> EventLoopFuture<Star.Output> {
try RelatedResourceController<Star.Output>().delete(
req: req,
relationKeyPath: \Galaxy.$stars)
}

func patch(req: Request) throws -> EventLoopFuture<Star.Output> {
try RelatedResourceController<Star.Output>().patch(
req: req,
using: Star.PatchInput.self,
relationKeyPath: \Galaxy.$stars)
}

func index(req: Request) throws -> EventLoopFuture<CursorPage<Star.Output>> {
try RelatedResourceController<Star.Output>().getCursorPage(
req: req,
relationKeyPath: \Galaxy.$stars)
}
}

```swift
let todos = routeBuilder.grouped("todos")
controller.addMethodsTo(todos, on: "tags")
```

This will add the following methods:
3. Routes setup:


| HTTP Method | Route | Result
| --------------------------- |:--------------------| :---------------|
|POST | /todos/:todoId/mentioned/tags | Create new
|GET | /todos/:todoId/mentioned/tags/:tagId | Show existing
|PUT | /todos/:todoId/mentioned/tags/:tagId | Update existing (Replace)
|PATCH | /todos/:todoId/mentioned/tags/:tagId | Patch exsiting (Partial update)
|DELETE | /todos/:todoId/mentioned/tags/:tagId | Delete
|GET | /todos/:todoId/mentioned/tags | Show list
```swift

app.group("galaxies") {

$0.group(Galaxy.idPath, "contains") {

$0.group("stars") {

let controller = StarForGalaxyNestedController()

$0.on(.POST, use: controller.create)
$0.on(.GET, Star.idPath, use: controller.read)
$0.on(.PUT, Star.idPath, use: controller.update)
$0.on(.PATCH, Star.idPath, use: controller.patch)
$0.on(.DELETE, Star.idPath, use: controller.delete)
$0.on(.GET, use: controller.index)

}
}
}
```

This will result in the following methods:

In case of nil provided as *relationName*, the following routes will be created:

| HTTP Method | Route | Result
| --------------------------- |:--------------------| :---------------|
|POST | /todos/:todoId/tags | Create new as related
|GET | /todos/:todoId/tags/:tagId | Show existing
|PUT | /todos/:todoId/tags/:tagId | Update existing (Replace)
|PATCH | /todos/:todoId/tags/:tagId | Patch exsiting (Partial update)
|DELETE | /todos/:todoId/tags/:tagId | Delete
|GET | /todos/:todoId/tags | Show list of related
|POST | /galaxies/:GalaxyId/contains/stars | Create new
|GET | /galaxies/:GalaxyId/contains/stars/:starId | Show existing
|PUT | /galaxies/:GalaxyId/contains/stars/:starId | Update existing (Replace)
|PATCH | /galaxies/:GalaxyId/contains/stars/:starId | Patch exsiting (Partial update)
|DELETE | /galaxies/:GalaxyId/contains/stars/:starId | Delete
|GET | /galaxies/:GalaxyId/contains/stars | Show list

### Inversed Siblings

Nested controllers for siblings work in both directions.
We can create:
- Tags controller for Tags related to a Todo
- Todo controller for Todos related to a Tag:

1. Create controller
```swift
let controller = Todo.Output
.controller(eagerLoading: EagerLoadingUnsupported.self)
.related(with: \Tag.$relatedTodos, relationName: "related")
.create(using: Todo.Input.self)
.read()
.update(using: Todo.Input.self)
.patch(using: Todo.PatchInput.self)
.read()
.delete()
.collection(sorting: DefaultSorting.self,
filtering: DefaultFiltering.self)
*Under the hood, there is a "resolver guy", who takes models' IDs from the route query path as `Galaxy.idPath` and `Star.idPath` and then looks them up in the database, taking into account specified relations key paths.*

Actually we can easily do without relation name part of the path:

```
2. Add methods to route builder
```swift
let tags = routeBuilder.grouped("tags")
controller.addMethodsTo(tags, on: "todos")
```
Will result in:

| HTTP Method | Route | Result
| --------------------------- |:--------------------| :---------------|
|POST | /tags/:tagId/related/todos | Create new
|GET | /tags/:tagId/related/todos/:todoId | Show existing
|PUT | /tags/:tagId/related/todos/:todoId | Update existing (Replace)
|PATCH | /tags/:tagId/related/todos/:todoId | Patch exsiting (Partial update)
|DELETE | /tags/:tagId/related/todos/:todoId | Delete
|GET | /tags/:tagId/related/todos | Show list

### Parent / Child relations

1. Create controller with child relation keyPath and optional *relationName*

```swift
let controller = Todo.Output
.controller(eagerLoading: EagerLoadingUnsupported.self)
.related(with: \User.$todos, relationName: "managed")
.create(using: Todo.Input.self)
.read()
.update(using: Todo.Input.self)
.patch(using: Todo.PatchInput.self)
.read()
.delete()
.collection(sorting: DefaultSorting.self,
filtering: DefaultFiltering.self)
app.group("galaxies") {

$0.group(Galaxy.idPath) {

$0.group("stars") {

let controller = StarForGalaxyNestedController()

$0.on(.POST, use: controller.create)
$0.on(.GET, Star.idPath, use: controller.read)
$0.on(.PUT, Star.idPath, use: controller.update)
$0.on(.PATCH, Star.idPath, use: controller.patch)
$0.on(.DELETE, Star.idPath, use: controller.delete)
$0.on(.GET, use: controller.index)

}
}
}

```

2. Add methods to route builder:

```swift
let users = routeBuilder.grouped("users")
controller.addMethodsTo(userss, on: "todos")

```


Will result in:
And get the following methods:

| HTTP Method | Route | Result
| --------------------------- |:--------------------| :---------------|
|POST | /users/:userId/managed/todos | Create new
|GET | /users/:userId/managed/todos/:todoId | Show existing
|PUT | /users/:userId/managed/todos/:todoId | Update existing (Replace)
|PATCH | /users/:userId/managed/todos/:todoId | Patch exsiting (Partial update)
|DELETE | /users/:userId/managed/todos/:todoId | Delete
|GET | /users/:userId/managed/todos | Show list


### Child / Parent relations
Probably more rare case, but still supported. Inversed nested controller for child - parent relation

1. Create controller with child relation keyPath and optional *relationName*:

```swift
let controller = User.Output
.controller(eagerLoading: EagerLoadingUnsupported.self)
.related(with: \User.$todos, relationName: "author")
.read()
```


2. Add methods to route builder:
|POST | /galaxies/:GalaxyId/stars | Create new
|GET | /galaxies/:GalaxyId/stars/:starId | Show existing
|PUT | /galaxies/:GalaxyId/stars/:starId | Update existing (Replace)
|PATCH | /galaxies/:GalaxyId/stars/:starId | Patch exsiting (Partial update)
|DELETE | /galaxies/:GalaxyId/stars/:starId | Delete
|GET | /galaxies/:GalaxyId/stars | Show list

```swift
let users = routeBuilder.grouped("users")
controller.addMethodsTo(users, on: "todos")

```



Will result in:
### Supported relations type

| HTTP Method | Route | Result
| --------------------------- |:--------------------| :---------------|
|POST | /todos/:todoId/author/users | Create new
|GET | /todos/:todoId/author/users/:userId | Show existing
|PUT | /todos/:todoId/author/users/:userId | Update existing (Replace)
|PATCH | /todos/:todoId/author/users/:userId | Patch exsiting (Partial update)
|DELETE | /todos/:todoId/author/users/:userId | Delete
|GET | /todos/:todoId/author/users | Show list
`RelatedResourceController` supports Child-Parent and Siblings relations in both directions.
Meaning that you can setup all kinds of "galaxy for star" or "stars for galaxy" controllers for parent-child relations. Along with "post-for-tags" or "tags-for-post" controllers for siblings relations.


### Related to Authenticatable Model
If root Model conforms to Vapor's Authenticatable protocol, it's possible to add **/me** nested controllers.
It works the same way as with other type of relations:

If root Model conforms to Vapor's Authenticatable protocol, there is a way to add related resource controller in a way that the root model would be resolved from authenticator istead of looked up by ID.
This a quick way for creating current user's posts or todos or whatever.


1. Create controller with relation keyPath and pass `.requireAuth()` as resolver parameter:

1. Create controller with relation keyPath, optional *relationName* and mention **authenticatable** type:

```swift
let controller = Todo.Output
.controller(eagerLoading: EagerLoadingUnsupported.self)
.related(with: \User.$todos, relationName: "managed")
.read(authenticatable: User.self)
.collection(authenticatable: User.self,
sorting: DefaultSorting.self,
filtering: DefaultFiltering.self)
struct MyTodosController {
func create(req: Request) throws -> EventLoopFuture<Todo.Output> {
try RelatedResourceController<Todo.Output>().create(
resolver: .requireAuth(),
req: req,
using: Todo.Input.self,
relationKeyPath: \User.$todos)
}

func read(req: Request) throws -> EventLoopFuture<Todo.Output> {
try RelatedResourceController<Todo.Output>().read(
resolver: .requireAuth(),
req: req,
relationKeyPath: \User.$todos)
}

func update(req: Request) throws -> EventLoopFuture<Todo.Output> {
try RelatedResourceController<Todo.Output>().update(
resolver: .requireAuth(),
req: req,
using: Todo.Input.self,
relationKeyPath: \User.$todos)
}

func delete(req: Request) throws -> EventLoopFuture<Todo.Output> {
try RelatedResourceController<Todo.Output>().delete(
resolver: .requireAuth(),
req: req,
relationKeyPath: \User.$todos)
}

func patch(req: Request) throws -> EventLoopFuture<Todo.Output> {
try RelatedResourceController<Todo.Output>().patch(
resolver: .requireAuth(),
req: req,
using: Todo.PatchInput.self,
relationKeyPath: \User.$todos)
}

func index(req: Request) throws -> EventLoopFuture<CursorPage<Todo.Output>> {
try RelatedResourceController<Todo.Output>().getCursorPage(
resolver: .requireAuth(),
req: req,
relationKeyPath: \User.$todos)
}
}


```
2. Make sure that auth and auth guard middlewares are added to the routee

```swift
authRoutesBuilder = routeBuilder.grouped(Authenticator(), User.guardMiddleware())
```

3. Add methods to route builder:
2. Setup route

```swift
let users = authRoutesBuilder.grouped("users")
controller.addMethodsTo(userss, on: "todos")

```
app.group("users") {

// Make sure that authenticator and auth guard middlewares are added to the route builder

let auth = $0.grouped(Authenticator(), User.guardMiddleware())

auth.group("me", "todos") {

let controller = MyTodosController()

$0.on(.POST, use: controller.create)
$0.on(.GET, Todo.idPath, use: controller.read)
$0.on(.PUT, Todo.idPath, use: controller.update)
$0.on(.PATCH, Todo.idPath, use: controller.patch)
$0.on(.DELETE, Todo.idPath, use: controller.delete)
$0.on(.GET, use: controller.index)
}
}

```

Will result in:

| HTTP Method | Route | Result
| --------------------------- |:--------------------| :---------------|
|POST | /users/me/managed/todos | Create new
|GET | /users/me/managed/todos/:todoId | Show existing
|PUT | /users/me/managed/todos/:todoId | Update existing (Replace)
|PATCH | /users/me/managed/todos/:todoId | Patch exsiting (Partial update)
|DELETE | /users/me/managed/todos/:todoId | Delete
|GET | /users/me/managed/todos | Show list
|POST | /users/me/todos | Create new
|GET | /users/me/todos/:todoId | Show existing
|PUT | /users/me/todos/:todoId | Update existing (Replace)
|PATCH | /users/me/todos/:todoId | Patch exsiting (Partial update)
|DELETE | /users/me/todos/:todoId | Delete
|GET | /users/me/todos | Show list
Loading

0 comments on commit e4fed30

Please sign in to comment.