diff --git a/.github/translation_needed.description.leaf b/.github/translation_needed.description.leaf index 0b7b37380..a58cd3820 100644 --- a/.github/translation_needed.description.leaf +++ b/.github/translation_needed.description.leaf @@ -10,6 +10,7 @@ Languages: - [ ] 한국어 (Korean) - [ ] Nederlands (Dutch) - [ ] Polski (Polish) +- [ ] Português Brasileiro (Brazilian Portuguese) - [ ] 简体中文 (Simplified Chinese) Assigned to @vapor/translators - please submit a PR with the relevant updates and check the box once merged. diff --git a/docs/advanced/apns.pt.md b/docs/advanced/apns.pt.md new file mode 100644 index 000000000..3ef012f8a --- /dev/null +++ b/docs/advanced/apns.pt.md @@ -0,0 +1,148 @@ +# APNS + +A API do Apple Push Notification Service (APNS) do Vapor facilita a autenticação e o envio de notificações push para dispositivos Apple. É construída sobre o [APNSwift](https://github.com/swift-server-community/APNSwift). + +## Primeiros Passos + +Vamos ver como você pode começar a usar o APNS. + +### Package + +O primeiro passo para usar o APNS é adicionar o pacote às suas dependências. + +```swift +// swift-tools-version:5.8 +import PackageDescription + +let package = Package( + name: "my-app", + dependencies: [ + // Outras dependências... + .package(url: "https://github.com/vapor/apns.git", from: "4.0.0"), + ], + targets: [ + .target(name: "App", dependencies: [ + // Outras dependências... + .product(name: "VaporAPNS", package: "apns") + ]), + // Outros targets... + ] +) +``` + +Se você editar o manifesto diretamente dentro do Xcode, ele automaticamente detectará as mudanças e buscará a nova dependência quando o arquivo for salvo. Caso contrário, no Terminal, execute `swift package resolve` para buscar a nova dependência. + +### Configuração + +O módulo APNS adiciona uma nova propriedade `apns` ao `Application`. Para enviar notificações push, você precisará definir a propriedade `configuration` com suas credenciais. + +```swift +import APNS +import VaporAPNS +import APNSCore + +// Configurar APNS usando autenticação JWT. +let apnsConfig = APNSClientConfiguration( + authenticationMethod: .jwt( + privateKey: try .loadFrom(string: "<#key.p8 content#>"), + keyIdentifier: "<#key identifier#>", + teamIdentifier: "<#team identifier#>" + ), + environment: .development +) +app.apns.containers.use( + apnsConfig, + eventLoopGroupProvider: .shared(app.eventLoopGroup), + responseDecoder: JSONDecoder(), + requestEncoder: JSONEncoder(), + as: .default +) +``` + +Preencha os placeholders com suas credenciais. O exemplo acima mostra [autenticação baseada em JWT](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_token-based_connection_to_apns) usando a chave `.p8` que você obtém do portal de desenvolvedores da Apple. Para [autenticação baseada em TLS](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_certificate-based_connection_to_apns) com um certificado, use o método de autenticação `.tls`: + +```swift +authenticationMethod: .tls( + privateKeyPath: <#path to private key#>, + pemPath: <#path to pem file#>, + pemPassword: <#optional pem password#> +) +``` + +### Envio + +Uma vez que o APNS está configurado, você pode enviar notificações push usando o método `apns.send` no `Application` ou `Request`. + +```swift +// Payload Codable personalizado +struct Payload: Codable { + let acme1: String + let acme2: Int +} +// Criar Alert de notificação push +let dt = "70075697aa918ebddd64efb165f5b9cb92ce095f1c4c76d995b384c623a258bb" +let payload = Payload(acme1: "hey", acme2: 2) +let alert = APNSAlertNotification( + alert: .init( + title: .raw("Olá"), + subtitle: .raw("Este é um teste do vapor/apns") + ), + expiration: .immediately, + priority: .immediately, + topic: "<#my topic#>", + payload: payload +) +// Enviar a notificação +try! await req.apns.client.sendAlertNotification( + alert, + deviceToken: dt, + deadline: .distantFuture +) +``` + +Use `req.apns` sempre que estiver dentro de um route handler. + +```swift +// Envia uma notificação push. +app.get("test-push") { req async throws -> HTTPStatus in + try await req.apns.client.send(...) + return .ok +} +``` + +O primeiro parâmetro aceita o alert de notificação push e o segundo parâmetro é o device token de destino. + +## Alert + +`APNSAlertNotification` é o metadado real do alert de notificação push a ser enviado. Mais detalhes sobre as especificidades de cada propriedade são fornecidos [aqui](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html). Eles seguem um esquema de nomenclatura um-para-um listado na documentação da Apple. + +```swift +let alert = APNSAlertNotification( + alert: .init( + title: .raw("Olá"), + subtitle: .raw("Este é um teste do vapor/apns") + ), + expiration: .immediately, + priority: .immediately, + topic: "<#my topic#>", + payload: payload +) +``` + +Este tipo pode ser passado diretamente ao método `send`. + +### Dados de Notificação Personalizados + +A Apple fornece aos engenheiros a capacidade de adicionar dados de payload personalizados a cada notificação. Para facilitar isso, aceitamos conformidade `Codable` no parâmetro payload em todas as APIs `send`. + +```swift +// Payload Codable personalizado +struct Payload: Codable { + let acme1: String + let acme2: Int +} +``` + +## Mais Informações + +Para mais informações sobre métodos disponíveis, veja o [README do APNSwift](https://github.com/swift-server-community/APNSwift). diff --git a/docs/advanced/commands.pt.md b/docs/advanced/commands.pt.md new file mode 100644 index 000000000..dd866875c --- /dev/null +++ b/docs/advanced/commands.pt.md @@ -0,0 +1,129 @@ +# Comandos + +A API de Comandos do Vapor permite que você construa funções personalizadas de linha de comando e interaja com o terminal. É sobre ela que os comandos padrão do Vapor como `serve`, `routes` e `migrate` são construídos. + +## Comandos Padrão + +Você pode aprender mais sobre os comandos padrão do Vapor usando a opção `--help`. + +```sh +swift run App --help +``` + +Você pode usar `--help` em um comando específico para ver quais argumentos e opções ele aceita. + +```sh +swift run App serve --help +``` + +### Xcode + +Você pode executar comandos no Xcode adicionando argumentos ao scheme `App`. Para fazer isso, siga estes passos: + +- Escolha o scheme `App` (à direita dos botões play/stop) +- Clique em "Edit Scheme" +- Escolha o produto "App" +- Selecione a aba "Arguments" +- Adicione o nome do comando em "Arguments Passed On Launch" (ex: `serve`) + +## Comandos Personalizados + +Você pode criar seus próprios comandos criando tipos que conformam com `AsyncCommand`. + +```swift +import Vapor + +struct HelloCommand: AsyncCommand { + ... +} +``` + +Adicionar o comando personalizado a `app.asyncCommands` o tornará disponível via `swift run`. + +```swift +app.asyncCommands.use(HelloCommand(), as: "hello") +``` + +Para conformar com `AsyncCommand`, você deve implementar o método `run`. Isso requer declarar uma `Signature`. Você também deve fornecer um texto de ajuda padrão. + +```swift +import Vapor + +struct HelloCommand: AsyncCommand { + struct Signature: CommandSignature { } + + var help: String { + "Diz olá" + } + + func run(using context: CommandContext, signature: Signature) async throws { + context.console.print("Olá, mundo!") + } +} +``` + +Este exemplo simples de comando não tem argumentos ou opções, então deixe a signature vazia. + +Você pode acessar o console atual através do contexto fornecido. O Console possui muitos métodos úteis para solicitar entrada do usuário, formatação de saída e mais. + +```swift +let name = context.console.ask("Qual é o seu \("nome", color: .blue)?") +context.console.print("Olá, \(name) 👋") +``` + +Teste seu comando executando: + +```sh +swift run App hello +``` + +### Cowsay + +Veja esta recriação do famoso comando [`cowsay`](https://en.wikipedia.org/wiki/Cowsay) para um exemplo de uso de `@Argument` e `@Option`. + +```swift +import Vapor + +struct Cowsay: AsyncCommand { + struct Signature: CommandSignature { + @Argument(name: "message") + var message: String + + @Option(name: "eyes", short: "e") + var eyes: String? + + @Option(name: "tongue", short: "t") + var tongue: String? + } + + var help: String { + "Gera uma imagem ASCII de uma vaca com uma mensagem." + } + + func run(using context: CommandContext, signature: Signature) async throws { + let eyes = signature.eyes ?? "oo" + let tongue = signature.tongue ?? " " + let cow = #""" + < $M > + \ ^__^ + \ ($E)\_______ + (__)\ )\/\ + $T ||----w | + || || + """#.replacingOccurrences(of: "$M", with: signature.message) + .replacingOccurrences(of: "$E", with: eyes) + .replacingOccurrences(of: "$T", with: tongue) + context.console.print(cow) + } +} +``` + +Tente adicionar isso à sua aplicação e executar. + +```swift +app.asyncCommands.use(Cowsay(), as: "cowsay") +``` + +```sh +swift run App cowsay sup --eyes ^^ --tongue "U " +``` diff --git a/docs/advanced/files.pt.md b/docs/advanced/files.pt.md new file mode 100644 index 000000000..ac70cc303 --- /dev/null +++ b/docs/advanced/files.pt.md @@ -0,0 +1,97 @@ +# Arquivos + +O Vapor oferece uma API simples para ler e escrever arquivos de forma assíncrona dentro de route handlers. Esta API é construída sobre o tipo [`NonBlockingFileIO`](https://swiftpackageindex.com/apple/swift-nio/main/documentation/nioposix/nonblockingfileio) do NIO. + +## Leitura + +O método principal para ler um arquivo entrega partes (chunks) a um callback handler conforme são lidas do disco. O arquivo a ser lido é especificado pelo seu caminho. Caminhos relativos procurarão no diretório de trabalho atual do processo. + +```swift +// Lê um arquivo do disco de forma assíncrona. +let readComplete: EventLoopFuture = req.fileio.readFile(at: "/caminho/do/arquivo") { chunk in + print(chunk) // ByteBuffer +} + +// Ou + +try await req.fileio.readFile(at: "/caminho/do/arquivo") { chunk in + print(chunk) // ByteBuffer +} +// Leitura completa +``` + +Se estiver usando `EventLoopFuture`s, o future retornado sinalizará quando a leitura foi concluída ou quando um erro ocorreu. Se estiver usando `async`/`await`, uma vez que o `await` retornar, a leitura foi concluída. Se um erro ocorrer, ele lançará um erro. + +### Stream + +O método `streamFile` converte um arquivo em streaming para uma `Response`. Este método definirá headers apropriados como `ETag` e `Content-Type` automaticamente. + +```swift +// Faz streaming de um arquivo como resposta HTTP de forma assíncrona. +req.fileio.streamFile(at: "/caminho/do/arquivo").map { res in + print(res) // Response +} + +// Ou + +let res = req.fileio.streamFile(at: "/caminho/do/arquivo") +print(res) + +``` + +O resultado pode ser retornado diretamente pelo seu route handler. + +### Collect + +O método `collectFile` lê o arquivo especificado em um buffer. + +```swift +// Lê o arquivo em um buffer. +req.fileio.collectFile(at: "/caminho/do/arquivo").map { buffer in + print(buffer) // ByteBuffer +} + +// ou + +let buffer = req.fileio.collectFile(at: "/caminho/do/arquivo") +print(buffer) +``` + +!!! warning "Aviso" + Este método requer que o arquivo inteiro esteja na memória de uma vez. Use leitura por chunks ou streaming para limitar o uso de memória. + +## Escrita + +O método `writeFile` suporta escrever um buffer em um arquivo. + +```swift +// Escreve buffer em um arquivo. +req.fileio.writeFile(ByteBuffer(string: "Olá, mundo"), at: "/caminho/do/arquivo") +``` + +O future retornado sinalizará quando a escrita foi concluída ou quando um erro ocorreu. + +## Middleware + +Para mais informações sobre servir arquivos da pasta _Public_ do seu projeto automaticamente, veja [Middleware → FileMiddleware](middleware.md#file-middleware). + +## Avançado + +Para casos que a API do Vapor não suporta, você pode usar o tipo `NonBlockingFileIO` do NIO diretamente. + +```swift +// Thread principal. +let fileHandle = try await app.fileio.openFile( + path: "/caminho/do/arquivo", + eventLoop: app.eventLoopGroup.next() +).get() +print(fileHandle) + +// Em um route handler. +let fileHandle = try await req.application.fileio.openFile( + path: "/caminho/do/arquivo", + eventLoop: req.eventLoop) +print(fileHandle) +``` + +Para mais informações, consulte a [referência de API](https://swiftpackageindex.com/apple/swift-nio/main/documentation/nioposix/nonblockingfileio) do SwiftNIO. diff --git a/docs/advanced/middleware.pt.md b/docs/advanced/middleware.pt.md new file mode 100644 index 000000000..05140ff10 --- /dev/null +++ b/docs/advanced/middleware.pt.md @@ -0,0 +1,152 @@ +# Middleware + +Middleware é uma cadeia lógica entre o cliente e um route handler do Vapor. Permite que você execute operações em requisições de entrada antes que cheguem ao route handler e em respostas de saída antes que cheguem ao cliente. + +## Configuração + +Middleware pode ser registrado globalmente (em toda rota) em `configure(_:)` usando `app.middleware`. + +```swift +app.middleware.use(MyMiddleware()) +``` + +Você também pode adicionar middleware a rotas individuais usando grupos de rotas. + +```swift +let group = app.grouped(MyMiddleware()) +group.get("foo") { req in + // Esta requisição passou pelo MyMiddleware. +} +``` + +### Ordem + +A ordem em que os middleware são adicionados é importante. Requisições chegando à sua aplicação passarão pelos middleware na ordem em que foram adicionados. Respostas saindo da sua aplicação passarão pelos middleware na ordem inversa. Middleware específicos de rota sempre rodam após middleware da aplicação. Veja o seguinte exemplo: + +```swift +app.middleware.use(MiddlewareA()) +app.middleware.use(MiddlewareB()) + +app.group(MiddlewareC()) { + $0.get("hello") { req in + "Olá, middleware." + } +} +``` + +Uma requisição para `GET /hello` passará pelos middleware na seguinte ordem: + +``` +Request → A → B → C → Handler → C → B → A → Response +``` + +Middleware também podem ser _prepended_, o que é útil quando você quer adicionar um middleware _antes_ do middleware padrão que o Vapor adiciona automaticamente: + +```swift +app.middleware.use(someMiddleware, at: .beginning) +``` + +## Criando um Middleware + +O Vapor vem com alguns middleware úteis, mas você pode precisar criar o seu próprio por causa dos requisitos da sua aplicação. Por exemplo, você poderia criar um middleware que impede qualquer usuário não-admin de acessar um grupo de rotas. + +> Recomendamos criar uma pasta `Middleware` dentro do seu diretório `Sources/App` para manter seu código organizado + +Middleware são tipos que conformam com o protocolo `Middleware` ou `AsyncMiddleware` do Vapor. Eles são inseridos na cadeia de responder e podem acessar e manipular uma requisição antes que chegue a um route handler e acessar e manipular uma resposta antes que ela seja retornada. + +Usando o exemplo mencionado acima, crie um middleware para bloquear o acesso ao usuário se ele não for um admin: + +```swift +import Vapor + +struct EnsureAdminUserMiddleware: Middleware { + func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { + guard let user = request.auth.get(User.self), user.role == .admin else { + return request.eventLoop.future(error: Abort(.unauthorized)) + } + return next.respond(to: request) + } +} +``` + +Ou se estiver usando `async`/`await` você pode escrever: + +```swift +import Vapor + +struct EnsureAdminUserMiddleware: AsyncMiddleware { + func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response { + guard let user = request.auth.get(User.self), user.role == .admin else { + throw Abort(.unauthorized) + } + return try await next.respond(to: request) + } +} +``` + +Se você quiser modificar a resposta, por exemplo para adicionar um header personalizado, você também pode usar um middleware para isso. Middleware podem aguardar até que a resposta seja recebida da cadeia de responder e manipular a resposta: + +```swift +import Vapor + +struct AddVersionHeaderMiddleware: Middleware { + func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { + next.respond(to: request).map { response in + response.headers.add(name: "My-App-Version", value: "v2.5.9") + return response + } + } +} +``` + +Ou se estiver usando `async`/`await` você pode escrever: + +```swift +import Vapor + +struct AddVersionHeaderMiddleware: AsyncMiddleware { + func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response { + let response = try await next.respond(to: request) + response.headers.add(name: "My-App-Version", value: "v2.5.9") + return response + } +} +``` + +## File Middleware + +`FileMiddleware` habilita o fornecimento de assets da pasta Public do seu projeto para o cliente. Você pode incluir arquivos estáticos como folhas de estilo ou imagens bitmap aqui. + +```swift +let file = FileMiddleware(publicDirectory: app.directory.publicDirectory) +app.middleware.use(file) +``` + +Uma vez que o `FileMiddleware` está registrado, um arquivo como `Public/images/logo.png` pode ser referenciado de um template Leaf como ``. + +Se seu servidor está contido em um Xcode Project, como um app iOS, use isso em vez disso: + +```swift +let file = try FileMiddleware(bundle: .main, publicDirectory: "Public") +``` + +Também certifique-se de usar Folder References em vez de Groups no Xcode para manter a estrutura de pastas nos recursos após compilar a aplicação. + +## CORS Middleware + +Cross-origin resource sharing (CORS) é um mecanismo que permite que recursos restritos em uma página web sejam solicitados de outro domínio fora do domínio de onde o primeiro recurso foi servido. APIs REST construídas no Vapor precisarão de uma política CORS para retornar requisições com segurança para navegadores web modernos. + +Um exemplo de configuração poderia ser algo assim: + +```swift +let corsConfiguration = CORSMiddleware.Configuration( + allowedOrigin: .all, + allowedMethods: [.GET, .POST, .PUT, .OPTIONS, .DELETE, .PATCH], + allowedHeaders: [.accept, .authorization, .contentType, .origin, .xRequestedWith, .userAgent, .accessControlAllowOrigin] +) +let cors = CORSMiddleware(configuration: corsConfiguration) +// O middleware CORS deve vir antes do middleware de erro padrão usando `at: .beginning` +app.middleware.use(cors, at: .beginning) +``` + +Dado que erros lançados são retornados imediatamente ao cliente, o `CORSMiddleware` deve ser listado _antes_ do `ErrorMiddleware`. Caso contrário, a resposta de erro HTTP será retornada sem headers CORS e não poderá ser lida pelo navegador. diff --git a/docs/advanced/queues.pt.md b/docs/advanced/queues.pt.md new file mode 100644 index 000000000..940c4ff57 --- /dev/null +++ b/docs/advanced/queues.pt.md @@ -0,0 +1,521 @@ +# Filas + +Vapor Queues ([vapor/queues](https://github.com/vapor/queues)) é um sistema de filas puro em Swift que permite descarregar a responsabilidade de tarefas para um worker separado. + +Algumas das tarefas para as quais este pacote funciona bem: + +- Envio de e-mails fora da thread principal de requisição +- Realização de operações complexas ou longas no banco de dados +- Garantia de integridade e resiliência de jobs +- Aceleração do tempo de resposta adiando processamento não crítico +- Agendamento de jobs para ocorrer em um horário específico + +Este pacote é similar ao [Ruby Sidekiq](https://github.com/mperham/sidekiq). Ele fornece as seguintes funcionalidades: + +- Tratamento seguro de sinais `SIGTERM` e `SIGINT` enviados por provedores de hospedagem para indicar shutdown, reinício ou novo deploy. +- Diferentes prioridades de filas. Por exemplo, você pode especificar um job de fila para rodar na fila de e-mail e outro job para rodar na fila de processamento de dados. +- Implementa o processo de fila confiável para ajudar com falhas inesperadas. +- Inclui uma funcionalidade `maxRetryCount` que repetirá o job até que ele tenha sucesso, até uma contagem especificada. +- Usa NIO para utilizar todos os cores e EventLoops disponíveis para jobs. +- Permite que usuários agendem tarefas recorrentes + +Queues atualmente possui um driver oficialmente suportado que faz interface com o protocolo principal: + +- [QueuesRedisDriver](https://github.com/vapor/queues-redis-driver) + +Queues também possui drivers baseados na comunidade: + +- [QueuesMongoDriver](https://github.com/vapor-community/queues-mongo-driver) +- [QueuesFluentDriver](https://github.com/vapor-community/vapor-queues-fluent-driver) + +!!! tip "Dica" + Você não deve instalar o pacote `vapor/queues` diretamente, a menos que esteja construindo um novo driver. Instale um dos pacotes de driver em vez disso. + +## Primeiros Passos + +Vamos ver como você pode começar a usar o Queues. + +### Package + +O primeiro passo para usar o Queues é adicionar um dos drivers como dependência ao seu projeto no arquivo de manifesto do SwiftPM. Neste exemplo, usaremos o driver Redis. + +```swift +// swift-tools-version:5.8 +import PackageDescription + +let package = Package( + name: "MyApp", + dependencies: [ + /// Quaisquer outras dependências... + .package(url: "https://github.com/vapor/queues-redis-driver.git", from: "1.0.0"), + ], + targets: [ + .executableTarget(name: "App", dependencies: [ + // Outras dependências + .product(name: "QueuesRedisDriver", package: "queues-redis-driver") + ]), + .testTarget(name: "AppTests", dependencies: [.target(name: "App")]), + ] +) +``` + +Se você editar o manifesto diretamente dentro do Xcode, ele automaticamente detectará as mudanças e buscará a nova dependência quando o arquivo for salvo. Caso contrário, no Terminal, execute `swift package resolve` para buscar a nova dependência. + +### Configuração + +O próximo passo é configurar o Queues em `configure.swift`. Usaremos a biblioteca Redis como exemplo: + +```swift +import QueuesRedisDriver + +try app.queues.use(.redis(url: "redis://127.0.0.1:6379")) +``` + +### Registrando um `Job` + +Após modelar um job, você deve adicioná-lo à sua seção de configuração assim: + +```swift +// Registrar jobs +let emailJob = EmailJob() +app.queues.add(emailJob) +``` + +### Executando Workers como Processos + +Para iniciar um novo worker de fila, execute `swift run App queues`. Você também pode especificar um tipo específico de worker para executar: `swift run App queues --queue emails`. + +!!! tip "Dica" + Workers devem ficar rodando em produção. Consulte seu provedor de hospedagem para descobrir como manter processos de longa duração ativos. O Heroku, por exemplo, permite que você especifique "worker" dynos assim no seu Procfile: `worker: Run queues`. Com isso configurado, você pode iniciar workers na aba Dashboard/Resources, ou com `heroku ps:scale worker=1` (ou qualquer número de dynos preferido). + +### Executando Workers no Processo + +Para executar um worker no mesmo processo da sua aplicação (em vez de iniciar um servidor separado para lidar com isso), chame os métodos de conveniência no `Application`: + +```swift +try app.queues.startInProcessJobs(on: .default) +``` + +Para executar jobs agendados no processo, chame o seguinte método: + +```swift +try app.queues.startScheduledJobs() +``` + +!!! warning "Aviso" + Se você não iniciar o worker de fila via linha de comando ou o worker no processo, os jobs não serão despachados. + +## O Protocolo `Job` + +Jobs são definidos pelo protocolo `Job` ou `AsyncJob`. + +### Modelando um objeto `Job`: + +```swift +import Vapor +import Foundation +import Queues + +struct Email: Codable { + let to: String + let message: String +} + +struct EmailJob: Job { + typealias Payload = Email + + func dequeue(_ context: QueueContext, _ payload: Email) -> EventLoopFuture { + // Aqui é onde você enviaria o e-mail + return context.eventLoop.future() + } + + func error(_ context: QueueContext, _ error: Error, _ payload: Email) -> EventLoopFuture { + // Se você não quiser tratar erros, pode simplesmente retornar um future. Você também pode omitir esta função inteiramente. + return context.eventLoop.future() + } +} +``` + +Se estiver usando `async`/`await`, você deve usar `AsyncJob`: + +```swift +struct EmailJob: AsyncJob { + typealias Payload = Email + + func dequeue(_ context: QueueContext, _ payload: Email) async throws { + // Aqui é onde você enviaria o e-mail + } + + func error(_ context: QueueContext, _ error: Error, _ payload: Email) async throws { + // Se você não quiser tratar erros, pode simplesmente retornar. Você também pode omitir esta função inteiramente. + } +} +``` + +!!! info "Informação" + Certifique-se de que seu tipo `Payload` implementa o protocolo `Codable`. + +!!! tip "Dica" + Não esqueça de seguir as instruções em **Primeiros Passos** para adicionar este job ao seu arquivo de configuração. + +## Despachando Jobs + +Para despachar um job de fila, você precisa de acesso a uma instância de `Application` ou `Request`. Você provavelmente estará despachando jobs dentro de um route handler: + +```swift +app.get("email") { req -> EventLoopFuture in + return req + .queue + .dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "mensagem") + ).map { "feito" } +} + +// ou + +app.get("email") { req async throws -> String in + try await req.queue.dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "mensagem")) + return "feito" +} +``` + +Se, em vez disso, você precisar despachar um job de um contexto onde o objeto `Request` não está disponível (como, por exemplo, de dentro de um `Command`), você precisará usar a propriedade `queues` dentro do objeto `Application`, assim: + +```swift +struct SendEmailCommand: AsyncCommand { + func run(using context: CommandContext, signature: Signature) async throws { + context + .application + .queues + .queue + .dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "mensagem") + ) + } +} +``` + +### Definindo `maxRetryCount` + +Jobs serão automaticamente reexecutados em caso de erro se você especificar um `maxRetryCount`. Por exemplo: + +```swift +app.get("email") { req -> EventLoopFuture in + return req + .queue + .dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "mensagem"), + maxRetryCount: 3 + ).map { "feito" } +} + +// ou + +app.get("email") { req async throws -> String in + try await req.queue.dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "mensagem"), + maxRetryCount: 3) + return "feito" +} +``` + +### Especificando um Atraso + +Jobs também podem ser configurados para rodar somente após uma certa `Date` ter passado. Para especificar um atraso, passe uma `Date` no parâmetro `delayUntil` no `dispatch`: + +```swift +app.get("email") { req async throws -> String in + let futureDate = Date(timeIntervalSinceNow: 60 * 60 * 24) // Um dia + try await req.queue.dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "mensagem"), + maxRetryCount: 3, + delayUntil: futureDate) + return "feito" +} +``` + +Se um job for retirado da fila antes do seu parâmetro de atraso, o job será re-enfileirado pelo driver. + +### Especificar uma Prioridade + +Jobs podem ser classificados em diferentes tipos/prioridades de fila dependendo das suas necessidades. Por exemplo, você pode querer abrir uma fila `email` e uma fila `background-processing` para classificar jobs. + +Comece estendendo `QueueName`: + +```swift +extension QueueName { + static let emails = QueueName(string: "emails") +} +``` + +Você também pode definir um `workerCount` por fila ao criar um `QueueName`: + +```swift +extension QueueName { + static let serialEmails = QueueName(string: "serial-emails", workerCount: 1) +} +``` + +Definir `workerCount: 1` faz com que essa fila processe jobs consecutivamente, o que é útil quando a ordem dos jobs importa. + +Então, especifique o tipo de fila ao recuperar o objeto `jobs`: + +```swift +app.get("email") { req -> EventLoopFuture in + let futureDate = Date(timeIntervalSinceNow: 60 * 60 * 24) // Um dia + return req + .queues(.emails) + .dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "mensagem"), + maxRetryCount: 3, + delayUntil: futureDate + ).map { "feito" } +} + +// ou + +app.get("email") { req async throws -> String in + let futureDate = Date(timeIntervalSinceNow: 60 * 60 * 24) // Um dia + try await req + .queues(.emails) + .dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "mensagem"), + maxRetryCount: 3, + delayUntil: futureDate + ) + return "feito" +} +``` + +Ao acessar de dentro do objeto `Application`, você deve fazer o seguinte: + +```swift +struct SendEmailCommand: AsyncCommand { + func run(using context: CommandContext, signature: Signature) async throws { + context + .application + .queues + .queue(.emails) + .dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "mensagem"), + maxRetryCount: 3, + delayUntil: futureDate + ) + } +} +``` + +Se você não especificar uma fila, o job será executado na fila `default`. Certifique-se de seguir as instruções em **Primeiros Passos** para iniciar workers para cada tipo de fila. + +## Agendando Jobs + +O pacote Queues também permite que você agende jobs para ocorrer em determinados momentos. + +!!! warning "Aviso" + Jobs agendados só funcionam quando configurados antes da aplicação iniciar, como em `configure.swift`. Eles não funcionarão em route handlers. + +### Iniciando o Worker do Agendador + +O agendador requer um processo de worker separado rodando, similar ao worker de fila. Você pode iniciar o worker executando este comando: + +```sh +swift run App queues --scheduled +``` + +!!! tip "Dica" + Workers devem ficar rodando em produção. Consulte seu provedor de hospedagem para descobrir como manter processos de longa duração ativos. O Heroku, por exemplo, permite que você especifique "worker" dynos assim no seu Procfile: `worker: App queues --scheduled` + +### Criando um `ScheduledJob` + +Para começar, crie um novo `ScheduledJob` ou `AsyncScheduledJob`: + +```swift +import Vapor +import Queues + +struct CleanupJob: ScheduledJob { + // Adicione serviços extras aqui via injeção de dependência, se necessário. + + func run(context: QueueContext) -> EventLoopFuture { + // Faça algum trabalho aqui, talvez enfileire outro job. + return context.eventLoop.makeSucceededFuture(()) + } +} + +struct CleanupJob: AsyncScheduledJob { + // Adicione serviços extras aqui via injeção de dependência, se necessário. + + func run(context: QueueContext) async throws { + // Faça algum trabalho aqui, talvez enfileire outro job. + } +} +``` + +Então, no seu código de configuração, registre o job agendado: + +```swift +app.queues.schedule(CleanupJob()) + .yearly() + .in(.may) + .on(23) + .at(.noon) +``` + +O job no exemplo acima será executado todo ano em 23 de maio às 12:00 PM. + +!!! tip "Dica" + O Agendador usa o fuso horário do seu servidor. + +### Métodos Builder Disponíveis + +Existem dois estilos de APIs do agendador: + +- Builders estilo calendário que retornam objetos builder para encadeamento. +- Builders estilo intervalo que executam jobs a cada duração fixa. + +Você deve continuar construindo uma cadeia de agendador estilo calendário até que o compilador não dê um aviso sobre resultado não utilizado. Veja abaixo todos os métodos disponíveis: + +| Função Helper | Modificadores Disponíveis | Descrição | +|---------------|---------------------------------------|--------------------------------------------------------------------------------| +| `yearly()` | `in(_ month: Month) -> Monthly` | O mês para executar o job. Retorna um objeto `Monthly` para construção adicional. | +| `monthly()` | `on(_ day: Day) -> Daily` | O dia para executar o job. Retorna um objeto `Daily` para construção adicional. | +| `weekly()` | `on(_ weekday: Weekday) -> Daily` | O dia da semana para executar o job. Retorna um objeto `Daily`. | +| `daily()` | `at(_ time: Time)` | O horário para executar o job. Método final da cadeia. | +| | `at(_ hour: Hour24, _ minute: Minute)`| A hora e minuto para executar o job. Método final da cadeia. | +| | `at(_ hour: Hour12, _ minute: Minute, _ period: HourPeriod)` | A hora, minuto e período para executar o job. Método final da cadeia. | +| `hourly()` | `at(_ minute: Minute)` | O minuto para executar o job. Método final da cadeia. | +| `minutely()` | `at(_ second: Second)` | O segundo para executar o job. Método final da cadeia. | + +### Métodos Builder de Intervalo (`.every(...)`) + +O agendador também suporta agendamento de intervalo fixo com os métodos `.every(...)`: + +| Função Helper | Descrição | +|-----------------------|----------------------------------------------------------------------| +| `every(seconds: Int)` | Executa o job a cada número dado de segundos. | +| `every(minutes: Int)` | Executa o job a cada número dado de minutos. | +| `every(hours: Int)` | Executa o job a cada número dado de horas. | +| `every(days: Int)` | Executa o job a cada número dado de dias. | +| `every(weeks: Int)` | Executa o job a cada número dado de semanas. | + +Exemplo: + +```swift +app.queues.schedule(CleanupJob()) + .every(hours: 6) +``` + +### Helpers Disponíveis + +O Queues vem com alguns enums helpers para facilitar o agendamento: + +| Função Helper | Enum Helper Disponível | +|---------------|---------------------------------------| +| `yearly()` | `.january`, `.february`, `.march`, ...| +| `monthly()` | `.first`, `.last`, `.exact(1)` | +| `weekly()` | `.sunday`, `.monday`, `.tuesday`, ... | +| `daily()` | `.midnight`, `.noon` | + +Para usar o enum helper, chame o modificador apropriado na função helper e passe o valor. Por exemplo: + +```swift +// Todo ano em janeiro +.yearly().in(.january) + +// Todo mês no primeiro dia +.monthly().on(.first) + +// Toda semana no domingo +.weekly().on(.sunday) + +// Todo dia à meia-noite +.daily().at(.midnight) +``` + +## Event Delegates + +O pacote Queues permite que você especifique objetos `JobEventDelegate` que receberão notificações quando o worker tomar ação em um job. Isso pode ser usado para monitoramento, geração de insights ou propósitos de alerta. + +Para começar, conforme um objeto com `JobEventDelegate` e implemente os métodos necessários + +```swift +struct MyEventDelegate: JobEventDelegate { + /// Chamado quando o job é despachado para o worker de fila a partir de uma rota + func dispatched(job: JobEventData, eventLoop: EventLoop) -> EventLoopFuture { + eventLoop.future() + } + + /// Chamado quando o job é colocado na fila de processamento e o trabalho começa + func didDequeue(jobId: String, eventLoop: EventLoop) -> EventLoopFuture { + eventLoop.future() + } + + /// Chamado quando o job terminou o processamento e foi removido da fila + func success(jobId: String, eventLoop: EventLoop) -> EventLoopFuture { + eventLoop.future() + } + + /// Chamado quando o job terminou o processamento mas teve um erro + func error(jobId: String, error: Error, eventLoop: EventLoop) -> EventLoopFuture { + eventLoop.future() + } +} +``` + +Então, adicione-o no seu arquivo de configuração: + +```swift +app.queues.add(MyEventDelegate()) +``` + +Existem vários pacotes de terceiros que usam a funcionalidade de delegate para fornecer insights adicionais sobre seus workers de fila: + +- [QueuesDatabaseHooks](https://github.com/vapor-community/queues-database-hooks) +- [QueuesDash](https://github.com/gotranseo/queues-dash) + +## Testes + +Para evitar problemas de sincronização e garantir testes determinísticos, o pacote Queues fornece uma biblioteca `XCTQueue` e um driver `AsyncTestQueuesDriver` dedicado a testes que você pode usar da seguinte forma: + +```swift +final class UserCreationServiceTests: XCTestCase { + var app: Application! + + override func setUp() async throws { + self.app = try await Application.make(.testing) + try await configure(app) + + // Sobrescrever o driver sendo usado para testes + app.queues.use(.asyncTest) + } + + override func tearDown() async throws { + try await self.app.asyncShutdown() + self.app = nil + } +} +``` + +Veja mais detalhes no [post de blog de Romain Pouclet](https://romain.codes/2024/10/08/using-and-testing-vapor-queues/). + +# Solução de Problemas + +Ao usar [queues-redis-driver](https://github.com/vapor/queues-redis-driver) com um servidor compatível com Redis baseado em cluster, como Redis ou Valkey na Amazon AWS, você pode encontrar esta mensagem de erro: `CROSSSLOT Keys in request don't hash to the same slot`. + +Isso só acontece no modo cluster, porque o Redis ou Valkey não pode saber com certeza em qual nó do cluster armazenar os dados do job. + +Para corrigir isso, adicione uma [hash tag](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/#hash-tags) aos nomes das suas entradas de dados de job usando chaves nos nomes: + +```swift +app.queues.configuration.persistenceKey = "vapor-queues-{queues}" +``` diff --git a/docs/advanced/request.pt.md b/docs/advanced/request.pt.md new file mode 100644 index 000000000..5d7bbe0bd --- /dev/null +++ b/docs/advanced/request.pt.md @@ -0,0 +1,65 @@ +# Requisição + +O objeto [`Request`](https://api.vapor.codes/vapor/documentation/vapor/request) é passado para cada [route handler](../basics/routing.md). + +```swift +app.get("hello", ":name") { req -> String in + let name = req.parameters.get("name")! + return "Olá, \(name)!" +} +``` + +Ele é a janela principal para o restante da funcionalidade do Vapor. Contém APIs para o [corpo da requisição](../basics/content.md), [parâmetros de query](../basics/content.md#query), [logger](../basics/logging.md), [cliente HTTP](../basics/client.md), [Authenticator](../security/authentication.md) e mais. Acessar essa funcionalidade através da requisição mantém a computação no event loop correto e permite que seja mockado para testes. Você também pode adicionar seus próprios [serviços](../advanced/services.md) ao `Request` com extensions. + +A documentação completa da API para `Request` pode ser encontrada [aqui](https://api.vapor.codes/vapor/documentation/vapor/request). + +## Application + +A propriedade `Request.application` contém uma referência ao [`Application`](https://api.vapor.codes/vapor/documentation/vapor/application). Este objeto contém toda a configuração e funcionalidade principal da aplicação. A maioria deve ser configurada apenas em `configure.swift`, antes da aplicação iniciar completamente, e muitas das APIs de baixo nível não serão necessárias na maioria das aplicações. Uma das propriedades mais úteis é `Application.eventLoopGroup`, que pode ser usado para obter um `EventLoop` para processos que precisam de um novo através do método `any()`. Também contém o [`Environment`](../basics/environment.md). + +## Body + +Se você quiser acesso direto ao corpo da requisição como um `ByteBuffer`, pode usar `Request.body.data`. Isso pode ser usado para fazer streaming de dados do corpo da requisição para um arquivo (embora você deva usar a propriedade [`fileio`](../advanced/files.md) na requisição para isso) ou para outro cliente HTTP. + +## Cookies + +Embora a aplicação mais útil de cookies seja através das [sessões](../advanced/sessions.md#configuration) integradas, você também pode acessar cookies diretamente via `Request.cookies`. + +```swift +app.get("my-cookie") { req -> String in + guard let cookie = req.cookies["my-cookie"] else { + throw Abort(.badRequest) + } + if let expiration = cookie.expires, expiration < Date() { + throw Abort(.badRequest) + } + return cookie.string +} +``` + +## Headers + +Um objeto `HTTPHeaders` pode ser acessado em `Request.headers`. Ele contém todos os headers enviados com a requisição. Pode ser usado para acessar o header `Content-Type`, por exemplo. + +```swift +app.get("json") { req -> String in + guard let contentType = req.headers.contentType, contentType == .json else { + throw Abort(.badRequest) + } + return "JSON" +} +``` + +Veja documentação adicional para `HTTPHeaders` [aqui](https://swiftpackageindex.com/apple/swift-nio/2.56.0/documentation/niohttp1/httpheaders). O Vapor também adiciona várias extensões ao `HTTPHeaders` para facilitar o trabalho com os headers mais comumente usados; uma lista está disponível [aqui](https://api.vapor.codes/vapor/documentation/vapor/niohttp1/httpheaders#instance-properties) + +## Endereço IP + +O `SocketAddress` representando o cliente pode ser acessado via `Request.remoteAddress`, que pode ser útil para logging ou rate limiting usando a representação em string `Request.remoteAddress.ipAddress`. Pode não representar com precisão o endereço IP do cliente se a aplicação estiver atrás de um proxy reverso. + +```swift +app.get("ip") { req -> String in + return req.remoteAddress.ipAddress +} +``` + +Veja documentação adicional para `SocketAddress` [aqui](https://swiftpackageindex.com/apple/swift-nio/2.56.0/documentation/niocore/socketaddress). diff --git a/docs/advanced/server.pt.md b/docs/advanced/server.pt.md new file mode 100644 index 000000000..c8dadbb01 --- /dev/null +++ b/docs/advanced/server.pt.md @@ -0,0 +1,233 @@ +# Servidor + +O Vapor inclui um servidor HTTP de alto desempenho e assíncrono construído sobre o [SwiftNIO](https://github.com/apple/swift-nio). Este servidor suporta HTTP/1, HTTP/2 e upgrades de protocolo como [WebSockets](websockets.md). O servidor também suporta a habilitação de TLS (SSL). + +## Configuração + +O servidor HTTP padrão do Vapor pode ser configurado via `app.http.server`. + +```swift +// Suportar apenas HTTP/2 +app.http.server.configuration.supportVersions = [.two] +``` + +O servidor HTTP suporta várias opções de configuração. + +### Hostname + +O hostname controla em qual endereço o servidor aceitará novas conexões. O padrão é `127.0.0.1`. + +```swift +// Configurar hostname personalizado. +app.http.server.configuration.hostname = "dev.local" +``` + +O hostname da configuração do servidor pode ser sobrescrito passando a flag `--hostname` (`-H`) ao comando `serve` ou passando o parâmetro `hostname` para `app.server.start(...)`. + +```sh +# Sobrescrever hostname configurado. +swift run App serve --hostname dev.local +``` + +### Porta + +A opção de porta controla em qual porta no endereço especificado o servidor aceitará novas conexões. O padrão é `8080`. + +```swift +// Configurar porta personalizada. +app.http.server.configuration.port = 1337 +``` + +!!! info "Informação" + `sudo` pode ser necessário para vincular a portas menores que `1024`. Portas maiores que `65535` não são suportadas. + +A porta da configuração do servidor pode ser sobrescrita passando a flag `--port` (`-p`) ao comando `serve` ou passando o parâmetro `port` para `app.server.start(...)`. + +```sh +# Sobrescrever porta configurada. +swift run App serve --port 1337 +``` + +### Backlog + +O parâmetro `backlog` define o comprimento máximo para a fila de conexões pendentes. O padrão é `256`. + +```swift +// Configurar backlog personalizado. +app.http.server.configuration.backlog = 128 +``` + +### Reutilização de Endereço + +O parâmetro `reuseAddress` permite a reutilização de endereços locais. O padrão é `true`. + +```swift +// Desabilitar reutilização de endereço. +app.http.server.configuration.reuseAddress = false +``` + +### TCP No Delay + +Habilitar o parâmetro `tcpNoDelay` tentará minimizar o atraso de pacotes TCP. O padrão é `true`. + +```swift +// Minimizar atraso de pacotes. +app.http.server.configuration.tcpNoDelay = true +``` + +### Compressão de Resposta + +O parâmetro `responseCompression` controla a compressão de resposta HTTP usando gzip. O padrão é `.disabled`. + +```swift +// Habilitar compressão de resposta HTTP. +app.http.server.configuration.responseCompression = .enabled +``` + +Para especificar uma capacidade inicial de buffer, use o parâmetro `initialByteBufferCapacity`. + +```swift +.enabled(initialByteBufferCapacity: 1024) +``` + +### Descompressão de Requisição + +O parâmetro `requestDecompression` controla a descompressão de requisição HTTP usando gzip. O padrão é `.disabled`. + +```swift +// Habilitar descompressão de requisição HTTP. +app.http.server.configuration.requestDecompression = .enabled +``` + +Para especificar um limite de descompressão, use o parâmetro `limit`. O padrão é `.ratio(10)`. + +```swift +// Sem limite de tamanho de descompressão +.enabled(limit: .none) +``` + +As opções disponíveis são: + +- `size`: Tamanho máximo descomprimido em bytes. +- `ratio`: Tamanho máximo descomprimido como proporção dos bytes comprimidos. +- `none`: Sem limites de tamanho. + +Definir limites de tamanho de descompressão pode ajudar a prevenir que requisições HTTP maliciosamente comprimidas usem grandes quantidades de memória. + +### Pipelining + +O parâmetro `supportPipelining` habilita suporte para pipelining de requisição e resposta HTTP. O padrão é `false`. + +```swift +// Suportar pipelining HTTP. +app.http.server.configuration.supportPipelining = true +``` + +### Versões + +O parâmetro `supportVersions` controla quais versões HTTP o servidor usará. Por padrão, o Vapor suportará tanto HTTP/1 quanto HTTP/2 quando TLS estiver habilitado. Apenas HTTP/1 é suportado quando TLS está desabilitado. + +```swift +// Desabilitar suporte a HTTP/1. +app.http.server.configuration.supportVersions = [.two] +``` + +### TLS + +O parâmetro `tlsConfiguration` controla se TLS (SSL) está habilitado no servidor. O padrão é `nil`. + +```swift +// Habilitar TLS. +app.http.server.configuration.tlsConfiguration = .makeServerConfiguration( + certificateChain: try NIOSSLCertificate.fromPEMFile("/caminho/do/cert.pem").map { .certificate($0) }, + privateKey: .privateKey(try NIOSSLPrivateKey(file: "/caminho/da/key.pem", format: .pem)) +) +``` + +Para esta configuração compilar, você precisa adicionar `import NIOSSL` no topo do seu arquivo de configuração. Você também pode precisar adicionar NIOSSL como dependência no seu arquivo Package.swift. + +### Nome + +O parâmetro `serverName` controla o header `Server` em respostas HTTP de saída. O padrão é `nil`. + +```swift +// Adicionar header 'Server: vapor' às respostas. +app.http.server.configuration.serverName = "vapor" +``` + +## Comando Serve + +Para iniciar o servidor do Vapor, use o comando `serve`. Este comando será executado por padrão se nenhum outro comando for especificado. + +```swift +swift run App serve +``` + +O comando `serve` aceita os seguintes parâmetros: + +- `hostname` (`-H`): Sobrescreve o hostname configurado. +- `port` (`-p`): Sobrescreve a porta configurada. +- `bind` (`-b`): Sobrescreve o hostname e porta configurados unidos por `:`. + +Um exemplo usando a flag `--bind` (`-b`): + +```swift +swift run App serve -b 0.0.0.0:80 +``` + +Use `swift run App serve --help` para mais informações. + +O comando `serve` ouvirá os sinais `SIGTERM` e `SIGINT` para encerrar o servidor graciosamente. Use `ctrl+c` (`^c`) para enviar um sinal `SIGINT`. Quando o nível de log está definido como `debug` ou inferior, informações sobre o status do encerramento gracioso serão registradas. + +## Início Manual + +O servidor do Vapor pode ser iniciado manualmente usando `app.server`. + +```swift +// Iniciar o servidor do Vapor. +try app.server.start() +// Solicitar encerramento do servidor. +app.server.shutdown() +// Aguardar o servidor encerrar. +try app.server.onShutdown.wait() +``` + +## Servidores + +O servidor que o Vapor usa é configurável. Por padrão, o servidor HTTP integrado é usado. + +```swift +app.servers.use(.http) +``` + +### Servidor Personalizado + +O servidor HTTP padrão do Vapor pode ser substituído por qualquer tipo que conforme com `Server`. + +```swift +import Vapor + +final class MyServer: Server { + ... +} + +app.servers.use { app in + MyServer() +} +``` + +Servidores personalizados podem estender `Application.Servers.Provider` para sintaxe com ponto. + +```swift +extension Application.Servers.Provider { + static var myServer: Self { + .init { + $0.servers.use { app in + MyServer() + } + } + } +} + +app.servers.use(.myServer) +``` diff --git a/docs/advanced/services.pt.md b/docs/advanced/services.pt.md new file mode 100644 index 000000000..0ffcbe6c4 --- /dev/null +++ b/docs/advanced/services.pt.md @@ -0,0 +1,129 @@ +# Serviços + +A `Application` e `Request` do Vapor são construídas para serem estendidas pela sua aplicação e pacotes de terceiros. Novas funcionalidades adicionadas a esses tipos são frequentemente chamadas de serviços. + +## Somente Leitura + +O tipo mais simples de serviço é somente leitura. Esses serviços consistem em variáveis computadas ou métodos adicionados ao application ou request. + +```swift +import Vapor + +struct MyAPI { + let client: Client + + func foos() async throws -> [String] { ... } +} + +extension Request { + var myAPI: MyAPI { + .init(client: self.client) + } +} +``` + +Serviços somente leitura podem depender de qualquer serviço pré-existente, como `client` neste exemplo. Uma vez que a extensão foi adicionada, seu serviço personalizado pode ser usado como qualquer outra propriedade na requisição. + +```swift +req.myAPI.foos() +``` + +## Gravável + +Serviços que precisam de estado ou configuração podem utilizar o storage da `Application` e `Request` para armazenar dados. Vamos supor que você queira adicionar a seguinte struct `MyConfiguration` à sua aplicação. + +```swift +struct MyConfiguration { + var apiKey: String +} +``` + +Para usar o storage, você deve declarar uma `StorageKey`. + +```swift +struct MyConfigurationKey: StorageKey { + typealias Value = MyConfiguration +} +``` + +Este é um struct vazio com um typealias `Value` especificando qual tipo está sendo armazenado. Ao usar um tipo vazio como chave, você pode controlar qual código é capaz de acessar seu valor no storage. Se o tipo for internal ou private, apenas seu código poderá modificar o valor associado no storage. + +Finalmente, adicione uma extensão ao `Application` para obter e definir a struct `MyConfiguration`. + +```swift +extension Application { + var myConfiguration: MyConfiguration? { + get { + self.storage[MyConfigurationKey.self] + } + set { + self.storage[MyConfigurationKey.self] = newValue + } + } +} +``` + +Uma vez que a extensão é adicionada, você pode usar `myConfiguration` como uma propriedade normal no `Application`. + +```swift +app.myConfiguration = .init(apiKey: ...) +print(app.myConfiguration?.apiKey) +``` + +## Lifecycle + +A `Application` do Vapor permite que você registre lifecycle handlers. Estes permitem que você se conecte a eventos como inicialização e encerramento. + +```swift +// Imprime hello durante a inicialização. +struct Hello: LifecycleHandler { + // Chamado antes da aplicação inicializar. + func willBoot(_ app: Application) throws { + app.logger.info("Olá!") + } + + // Chamado após a aplicação inicializar. + func didBoot(_ app: Application) throws { + app.logger.info("Servidor está rodando") + } + + // Chamado antes do encerramento da aplicação. + func shutdown(_ app: Application) { + app.logger.info("Até logo!") + } +} + +// Adicionar lifecycle handler. +app.lifecycle.use(Hello()) +``` + +## Locks + +A `Application` do Vapor inclui conveniências para sincronizar código usando locks. Ao declarar uma `LockKey`, você pode obter um lock único e compartilhado para sincronizar o acesso ao seu código. + +```swift +struct TestKey: LockKey { } + +let test = app.locks.lock(for: TestKey.self) +test.withLock { + // Fazer algo. +} +``` + +Cada chamada a `lock(for:)` com a mesma `LockKey` retornará o mesmo lock. Este método é thread-safe. + +Para um lock de toda a aplicação, você pode usar `app.sync`. + +```swift +app.sync.withLock { + // Fazer algo. +} +``` + +## Request + +Serviços que devem ser usados em route handlers devem ser adicionados ao `Request`. Serviços de request devem usar o logger e event loop da requisição. É importante que uma requisição permaneça no mesmo event loop ou uma asserção será disparada quando a resposta for retornada ao Vapor. + +Se um serviço precisar sair do event loop da requisição para fazer trabalho, ele deve garantir o retorno ao event loop antes de finalizar. Isso pode ser feito usando o `hop(to:)` no `EventLoopFuture`. + +Serviços de request que precisam de acesso a serviços da aplicação, como configurações, podem usar `req.application`. Tenha cuidado ao considerar a thread-safety ao acessar a aplicação a partir de um route handler. Geralmente, apenas operações de leitura devem ser realizadas por requisições. Operações de escrita devem ser protegidas por locks. diff --git a/docs/advanced/sessions.pt.md b/docs/advanced/sessions.pt.md new file mode 100644 index 000000000..7e507bacd --- /dev/null +++ b/docs/advanced/sessions.pt.md @@ -0,0 +1,147 @@ +# Sessões + +Sessões permitem que você persista dados de um usuário entre múltiplas requisições. Sessões funcionam criando e retornando um cookie único junto com a resposta HTTP quando uma nova sessão é inicializada. Navegadores detectarão automaticamente este cookie e o incluirão em requisições futuras. Isso permite que o Vapor restaure automaticamente a sessão de um usuário específico no seu request handler. + +Sessões são ótimas para aplicações web front-end construídas no Vapor que servem HTML diretamente para navegadores web. Para APIs, recomendamos usar [autenticação stateless baseada em token](../security/authentication.md) para persistir dados do usuário entre requisições. + +## Configuração + +Para usar sessões em uma rota, a requisição deve passar pelo `SessionsMiddleware`. A forma mais fácil de conseguir isso é adicionando este middleware globalmente. É recomendado que você faça isso após declarar a factory de cookie. Isso porque Sessions é uma struct, portanto é um tipo por valor, e não um tipo por referência. Como é um tipo por valor, você deve definir o valor antes de usar o `SessionsMiddleware`. + +```swift +app.middleware.use(app.sessions.middleware) +``` + +Se apenas um subconjunto das suas rotas utiliza sessões, você pode adicionar o `SessionsMiddleware` a um grupo de rotas. + +```swift +let sessions = app.grouped(app.sessions.middleware) +``` + +O cookie HTTP gerado pelas sessões pode ser configurado usando `app.sessions.configuration`. Você pode alterar o nome do cookie e declarar uma função personalizada para gerar valores de cookie. + +```swift +// Alterar o nome do cookie para "foo". +app.sessions.configuration.cookieName = "foo" + +// Configura a criação de valores de cookie. +app.sessions.configuration.cookieFactory = { sessionID in + .init(string: sessionID.string, isSecure: true) +} + +app.middleware.use(app.sessions.middleware) +``` + +Por padrão, o Vapor usará `vapor_session` como nome do cookie. + +## Drivers + +Drivers de sessão são responsáveis por armazenar e recuperar dados de sessão por identificador. Você pode criar drivers personalizados conformando com o protocolo `SessionDriver`. + +!!! warning "Aviso" + O driver de sessão deve ser configurado _antes_ de adicionar `app.sessions.middleware` à sua aplicação. + +### In-Memory + +O Vapor utiliza sessões em memória por padrão. Sessões em memória não requerem configuração e não persistem entre reinicializações da aplicação, o que as torna ótimas para testes. Para habilitar sessões em memória manualmente, use `.memory`: + +```swift +app.sessions.use(.memory) +``` + +Para casos de uso em produção, veja os outros drivers de sessão que utilizam bancos de dados para persistir e compartilhar sessões entre múltiplas instâncias da sua aplicação. + +### Fluent + +O Fluent inclui suporte para armazenar dados de sessão no banco de dados da sua aplicação. Esta seção assume que você [configurou o Fluent](../fluent/overview.md) e pode se conectar a um banco de dados. O primeiro passo é habilitar o driver de sessões do Fluent. + +```swift +import Fluent + +app.sessions.use(.fluent) +``` + +Isso configurará as sessões para usar o banco de dados padrão da aplicação. Para especificar um banco de dados específico, passe o identificador do banco de dados. + +```swift +app.sessions.use(.fluent(.sqlite)) +``` + +Finalmente, adicione a migration do `SessionRecord` às migrations do seu banco de dados. Isso preparará seu banco de dados para armazenar dados de sessão no schema `_fluent_sessions`. + +```swift +app.migrations.add(SessionRecord.migration) +``` + +Certifique-se de executar as migrations da sua aplicação após adicionar a nova migration. As sessões agora serão armazenadas no banco de dados da sua aplicação, permitindo que persistam entre reinicializações e sejam compartilhadas entre múltiplas instâncias da sua aplicação. + +### Redis + +O Redis fornece suporte para armazenar dados de sessão na sua instância Redis configurada. Esta seção assume que você [configurou o Redis](../redis/overview.md) e pode enviar comandos para a instância Redis. + +Para usar o Redis para Sessões, selecione-o ao configurar sua aplicação: + +```swift +import Redis + +app.sessions.use(.redis) +``` + +Isso configurará as sessões para usar o driver de sessões Redis com o comportamento padrão. + +!!! seealso "Veja Também" + Consulte [Redis → Sessions](../redis/sessions.md) para informações mais detalhadas sobre Redis e Sessões. + +## Dados de Sessão + +Agora que as sessões estão configuradas, você está pronto para persistir dados entre requisições. Novas sessões são inicializadas automaticamente quando dados são adicionados a `req.session`. O route handler de exemplo abaixo aceita um parâmetro de rota dinâmico e adiciona o valor a `req.session.data`. + +```swift +app.get("set", ":value") { req -> HTTPStatus in + req.session.data["name"] = req.parameters.get("value") + return .ok +} +``` + +Use a seguinte requisição para inicializar uma sessão com o nome Vapor. + +```http +GET /set/vapor HTTP/1.1 +content-length: 0 +``` + +Você deverá receber uma resposta similar à seguinte: + +```http +HTTP/1.1 200 OK +content-length: 0 +set-cookie: vapor-session=123; Expires=Fri, 10 Apr 2020 21:08:09 GMT; Path=/ +``` + +Note que o header `set-cookie` foi adicionado automaticamente à resposta após adicionar dados a `req.session`. Incluir este cookie em requisições subsequentes permitirá acesso aos dados da sessão. + +Adicione o seguinte route handler para acessar o valor do nome a partir da sessão. + +```swift +app.get("get") { req -> String in + req.session.data["name"] ?? "n/a" +} +``` + +Use a seguinte requisição para acessar esta rota, certificando-se de passar o valor do cookie da resposta anterior. + +```http +GET /get HTTP/1.1 +cookie: vapor-session=123 +``` + +Você deverá ver o nome Vapor retornado na resposta. Você pode adicionar ou remover dados da sessão como desejar. Os dados da sessão serão sincronizados com o driver de sessão automaticamente antes de retornar a resposta HTTP. + +Para encerrar uma sessão, use `req.session.destroy`. Isso deletará os dados do driver de sessão e invalidará o cookie de sessão. + +```swift +app.get("del") { req -> HTTPStatus in + req.session.destroy() + return .ok +} +``` diff --git a/docs/advanced/testing.pt.md b/docs/advanced/testing.pt.md new file mode 100644 index 000000000..3dd3b27e8 --- /dev/null +++ b/docs/advanced/testing.pt.md @@ -0,0 +1,272 @@ +# Testes + +## VaporTesting + +O Vapor inclui um módulo chamado `VaporTesting` que fornece helpers de teste construídos sobre o `Swift Testing`. Esses helpers de teste permitem que você envie requisições de teste para sua aplicação Vapor programaticamente ou executando através de um servidor HTTP. + +!!! note "Nota" + Para projetos mais novos ou equipes adotando concorrência Swift, o `Swift Testing` é altamente recomendado em vez do `XCTest`. + +### Primeiros Passos + +Para usar o módulo `VaporTesting`, certifique-se de que ele foi adicionado ao target de teste do seu pacote. + +```swift +let package = Package( + ... + dependencies: [ + .package(url: "https://github.com/vapor/vapor.git", from: "4.110.1") + ], + targets: [ + ... + .testTarget(name: "AppTests", dependencies: [ + .target(name: "App"), + .product(name: "VaporTesting", package: "vapor"), + ]) + ] +) +``` + +!!! warning "Aviso" + Certifique-se de usar o módulo de teste correspondente, pois não fazê-lo pode resultar em falhas de teste do Vapor não sendo reportadas corretamente. + +Então, adicione `import VaporTesting` e `import Testing` no topo dos seus arquivos de teste. Crie structs com um nome `@Suite` para escrever casos de teste. + +```swift +@testable import App +import VaporTesting +import Testing + +@Suite("App Tests") +struct AppTests { + @Test("Test Stub") + func stub() async throws { + // Teste aqui. + } +} +``` + +Cada função marcada com `@Test` será executada automaticamente quando sua aplicação for testada. + +Para garantir que seus testes rodem de forma serializada (ex: ao testar com um banco de dados), inclua a opção `.serialized` na declaração do test suite: + +```swift +@Suite("App Tests with DB", .serialized) +``` + +### Testable Application + +Para fornecer uma configuração e desmontagem padronizadas e simplificadas dos testes, o `VaporTesting` oferece a função helper `withApp`. Este método encapsula o gerenciamento de ciclo de vida da instância `Application`, garantindo que a aplicação seja devidamente inicializada, configurada e encerrada para cada teste. + +Passe o método `configure(_:)` da sua aplicação para a função helper `withApp` para garantir que todas as suas rotas sejam corretamente registradas: + +```swift +@Test func someTest() async throws { + try await withApp(configure: configure) { app in + // seu teste real + } +} +``` + +#### Enviar Requisição + +Para enviar uma requisição de teste para sua aplicação, use o método privado `withApp` e dentro use o método `app.testing().test()`: + +```swift +@Test("Testar Rota Hello World") +func helloWorld() async throws { + try await withApp(configure: configure) { app in + try await app.testing().test(.GET, "hello") { res async in + #expect(res.status == .ok) + #expect(res.body.string == "Olá, mundo!") + } + } +} +``` + +Os dois primeiros parâmetros são o método HTTP e a URL a requisitar. A closure final aceita a resposta HTTP que você pode verificar usando a macro `#expect`. + +Para requisições mais complexas, você pode fornecer uma closure `beforeRequest` para modificar headers ou codificar conteúdo. A [API de Conteúdo](../basics/content.md) do Vapor está disponível tanto na requisição de teste quanto na resposta. + +```swift +let newDTO = TodoDTO(id: nil, title: "test") + +try await app.testing().test(.POST, "todos", beforeRequest: { req in + try req.content.encode(newDTO) +}, afterResponse: { res async throws in + #expect(res.status == .ok) + let models = try await Todo.query(on: app.db).all() + #expect(models.map({ $0.toDTO().title }) == [newDTO.title]) +}) +``` + +#### Método de Teste + +A API de teste do Vapor suporta enviar requisições de teste programaticamente e via um servidor HTTP ativo. Você pode especificar qual método deseja usar através do método `testing`. + +```swift +// Usar teste programático. +app.testing(method: .inMemory).test(...) + +// Executar testes através de um servidor HTTP ativo. +app.testing(method: .running).test(...) +``` + +A opção `inMemory` é usada por padrão. + +A opção `running` suporta passar uma porta específica para usar. Por padrão, `8080` é usada. + +```swift +app.testing(method: .running(port: 8123)).test(...) +``` + +#### Testes de Integração com Banco de Dados + +Configure o banco de dados especificamente para testes para garantir que seu banco de dados de produção nunca seja usado durante os testes. Por exemplo, ao usar SQLite, você poderia configurar seu banco de dados na função `configure(_:)` da seguinte forma: + +```swift +public func configure(_ app: Application) async throws { + // Todas as outras configurações... + + if app.environment == .testing { + app.databases.use(.sqlite(.memory), as: .sqlite) + } else { + app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite) + } +} +``` + +!!! warning "Aviso" + Certifique-se de executar seus testes contra o banco de dados correto, para evitar sobrescrever acidentalmente dados que você não quer perder. + +Então você pode aprimorar seus testes usando `autoMigrate()` e `autoRevert()` para gerenciar o schema do banco de dados e o ciclo de vida dos dados durante os testes. Para isso, você deve criar sua própria função helper `withAppIncludingDB` que inclui os ciclos de vida do schema e dados do banco: + +```swift +private func withAppIncludingDB(_ test: (Application) async throws -> ()) async throws { + let app = try await Application.make(.testing) + do { + try await configure(app) + try await app.autoMigrate() + try await test(app) + try await app.autoRevert() + } + catch { + try? await app.autoRevert() + try await app.asyncShutdown() + throw error + } + try await app.asyncShutdown() +} +``` + +E então use este helper nos seus testes: +```swift +@Test func myDatabaseIntegrationTest() async throws { + try await withAppIncludingDB { app in + try await app.testing().test(.GET, "hello") { res async in + #expect(res.status == .ok) + #expect(res.body.string == "Olá, mundo!") + } + } +} +``` + +Ao combinar esses métodos, você pode garantir que cada teste comece com um estado de banco de dados limpo e consistente, tornando seus testes mais confiáveis e reduzindo a probabilidade de falsos positivos ou negativos causados por dados remanescentes. + + +## XCTVapor + +O Vapor inclui um módulo chamado `XCTVapor` que fornece helpers de teste construídos sobre o `XCTest`. Esses helpers de teste permitem que você envie requisições de teste para sua aplicação Vapor programaticamente ou executando através de um servidor HTTP. + +### Primeiros Passos + +Para usar o módulo `XCTVapor`, certifique-se de que ele foi adicionado ao target de teste do seu pacote. + +```swift +let package = Package( + ... + dependencies: [ + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0") + ], + targets: [ + ... + .testTarget(name: "AppTests", dependencies: [ + .target(name: "App"), + .product(name: "XCTVapor", package: "vapor"), + ]) + ] +) +``` + +Então, adicione `import XCTVapor` no topo dos seus arquivos de teste. Crie classes estendendo `XCTestCase` para escrever casos de teste. + +```swift +import XCTVapor + +final class MyTests: XCTestCase { + func testStub() throws { + // Teste aqui. + } +} +``` + +Cada função começando com `test` será executada automaticamente quando sua aplicação for testada. + +### Testable Application + +Inicialize uma instância de `Application` usando o environment `.testing`. Você deve chamar `app.shutdown()` antes que esta aplicação seja desinicializada. + +O shutdown é necessário para ajudar a liberar os recursos que a aplicação requisitou. Em particular, é importante liberar as threads que a aplicação solicita na inicialização. Se você não chamar `shutdown()` na aplicação após cada teste unitário, você pode encontrar sua suite de testes falhando com uma precondition failure ao alocar threads para uma nova instância de `Application`. + +```swift +let app = Application(.testing) +defer { app.shutdown() } +try configure(app) +``` + +Passe a `Application` para o método `configure(_:)` do seu pacote para aplicar sua configuração. Quaisquer configurações somente para teste podem ser aplicadas após. + +#### Enviar Requisição + +Para enviar uma requisição de teste para sua aplicação, use o método `test`. + +```swift +try app.test(.GET, "hello") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "Olá, mundo!") +} +``` + +Os dois primeiros parâmetros são o método HTTP e a URL a requisitar. A closure final aceita a resposta HTTP que você pode verificar usando métodos `XCTAssert`. + +Para requisições mais complexas, você pode fornecer uma closure `beforeRequest` para modificar headers ou codificar conteúdo. A [API de Conteúdo](../basics/content.md) do Vapor está disponível tanto na requisição de teste quanto na resposta. + +```swift +try app.test(.POST, "todos", beforeRequest: { req in + try req.content.encode(["title": "Test"]) +}, afterResponse: { res in + XCTAssertEqual(res.status, .created) + let todo = try res.content.decode(Todo.self) + XCTAssertEqual(todo.title, "Test") +}) +``` + +#### Método Testable + +A API de teste do Vapor suporta enviar requisições de teste programaticamente e via um servidor HTTP ativo. Você pode especificar qual método deseja usar utilizando o método `testable`. + +```swift +// Usar teste programático. +app.testable(method: .inMemory).test(...) + +// Executar testes através de um servidor HTTP ativo. +app.testable(method: .running).test(...) +``` + +A opção `inMemory` é usada por padrão. + +A opção `running` suporta passar uma porta específica para usar. Por padrão, `8080` é usada. + +```swift +.running(port: 8123) +``` diff --git a/docs/advanced/tracing.pt.md b/docs/advanced/tracing.pt.md new file mode 100644 index 000000000..2ccb9989e --- /dev/null +++ b/docs/advanced/tracing.pt.md @@ -0,0 +1,83 @@ +# Rastreamento + +Rastreamento é uma ferramenta poderosa para monitorar e depurar sistemas distribuídos. A API de rastreamento do Vapor permite que desenvolvedores rastreiem facilmente ciclos de vida de requisições, propaguem metadados e integrem com backends populares como OpenTelemetry. + +A API de rastreamento do Vapor é construída sobre o [swift-distributed-tracing](https://github.com/apple/swift-distributed-tracing), o que significa que é compatível com todas as [implementações de backend](https://github.com/apple/swift-distributed-tracing/blob/main/README.md#tracing-backends) do swift-distributed-tracing. + +Se você não está familiarizado com rastreamento e spans em Swift, revise a [documentação de Trace do OpenTelemetry](https://opentelemetry.io/docs/concepts/signals/traces/) e a [documentação do swift-distributed-tracing](https://swiftpackageindex.com/apple/swift-distributed-tracing/main/documentation/tracing). + +## TracingMiddleware + +Para criar automaticamente um span totalmente anotado para cada requisição, adicione o `TracingMiddleware` à sua aplicação. + +```swift +app.middleware.use(TracingMiddleware()) +``` + +Para obter medições precisas de span e garantir que os identificadores de rastreamento sejam passados corretamente para outros serviços, adicione este middleware antes dos outros middleware. + +## Adicionando Spans + +Ao adicionar spans a route handlers, é ideal que eles estejam associados ao span de requisição de nível superior. Isso é chamado de "propagação de span" e pode ser tratado de duas formas diferentes: automática ou manual. + +### Propagação Automática + +O Vapor tem suporte para propagar automaticamente spans entre middleware e callbacks de rotas. Para isso, defina a propriedade `Application.traceAutoPropagation` como true durante a configuração. + +```swift +app.traceAutoPropagation = true +``` + +!!! note "Nota" + Habilitar a propagação automática pode degradar o desempenho em APIs de alto throughput com necessidades mínimas de rastreamento, já que os metadados do span de requisição devem ser restaurados para cada route handler, independentemente de os spans serem criados. + +Então os spans podem ser criados na closure da rota usando a sintaxe comum de distributed tracing. + +```swift +app.get("fetchAndProcess") { req in + let result = try await fetch() + return try await withSpan("getNameParameter") { _ in + try await process(result) + } +} +``` + +### Propagação Manual + +Para evitar as implicações de desempenho da propagação automática, você pode restaurar manualmente os metadados do span onde necessário. O `TracingMiddleware` define automaticamente uma propriedade `Request.serviceContext` que pode ser usada diretamente no parâmetro `context` do `withSpan`. + +```swift +app.get("fetchAndProcess") { req in + let result = try await fetch() + return try await withSpan("getNameParameter", context: req.serviceContext) { _ in + try await process(result) + } +} +``` + +Para restaurar os metadados do span sem criar um span, use `ServiceContext.withValue`. Isso é valioso se você sabe que bibliotecas assíncronas downstream emitem seus próprios spans de rastreamento, e esses devem ser aninhados sob o span de requisição pai. + +```swift +app.get("fetchAndProcess") { req in + try await ServiceContext.withValue(req.serviceContext) { + try await fetch() + return try await process(result) + } +} +``` + +## Considerações sobre NIO + +Como o `swift-distributed-tracing` usa [`propriedades TaskLocal`](https://developer.apple.com/documentation/swift/tasklocal) para propagar, você deve restaurar manualmente o contexto sempre que cruzar fronteiras de `NIO EventLoopFuture` para garantir que os spans sejam vinculados corretamente. **Isso é necessário independentemente de a propagação automática estar habilitada**. + +```swift +app.get("fetchAndProcessNIO") { req in + withSpan("fetch", context: req.serviceContext) { span in + fetchSomething().map { result in + withSpan("process", context: span.context) { _ in + process(result) + } + } + } +} +``` diff --git a/docs/advanced/websockets.pt.md b/docs/advanced/websockets.pt.md new file mode 100644 index 000000000..e8ec163bf --- /dev/null +++ b/docs/advanced/websockets.pt.md @@ -0,0 +1,134 @@ +# WebSockets + +[WebSockets](https://en.wikipedia.org/wiki/WebSocket) permitem comunicação bidirecional entre um cliente e servidor. Diferente do HTTP, que tem um padrão de requisição e resposta, peers WebSocket podem enviar um número arbitrário de mensagens em qualquer direção. A API de WebSocket do Vapor permite que você crie tanto clientes quanto servidores que lidam com mensagens de forma assíncrona. + +## Servidor + +Endpoints WebSocket podem ser adicionados à sua aplicação Vapor existente usando a API de Roteamento. Use o método `webSocket` como você usaria `get` ou `post`. + +```swift +app.webSocket("echo") { req, ws in + // WebSocket conectado. + print(ws) +} +``` + +Rotas WebSocket podem ser agrupadas e protegidas por middleware como rotas normais. + +Além de aceitar a requisição HTTP de entrada, handlers WebSocket aceitam a conexão WebSocket recém-estabelecida. Veja abaixo para mais informações sobre como usar este WebSocket para enviar e ler mensagens. + +## Cliente + +Para conectar a um endpoint WebSocket remoto, use `WebSocket.connect`. + +```swift +WebSocket.connect(to: "ws://echo.websocket.org", on: eventLoop) { ws in + // WebSocket conectado. + print(ws) +} +``` + +O método `connect` retorna um future que se completa quando a conexão é estabelecida. Uma vez conectado, a closure fornecida será chamada com o WebSocket recém-conectado. Veja abaixo para mais informações sobre como usar este WebSocket para enviar e ler mensagens. + +## Mensagens + +A classe `WebSocket` possui métodos para enviar e receber mensagens, bem como ouvir eventos como fechamento. WebSockets podem transmitir dados via dois protocolos: texto e binário. Mensagens de texto são interpretadas como strings UTF-8, enquanto dados binários são interpretados como um array de bytes. + +### Envio + +Mensagens podem ser enviadas usando o método `send` do WebSocket. + +```swift +ws.send("Olá, mundo") +``` + +Passar uma `String` para este método resulta em uma mensagem de texto sendo enviada. Mensagens binárias podem ser enviadas passando um `[UInt8]`. + +```swift +ws.send([1, 2, 3]) +``` + +O envio de mensagens é assíncrono. Você pode fornecer um `EventLoopPromise` ao método send para ser notificado quando a mensagem terminou de ser enviada ou falhou. + +```swift +let promise = eventLoop.makePromise(of: Void.self) +ws.send(..., promise: promise) +promise.futureResult.whenComplete { result in + // Enviou com sucesso ou falhou. +} +``` + +Se estiver usando `async`/`await`, você pode usar `await` para aguardar a conclusão da operação assíncrona + +```swift +try await ws.send(...) +``` + +### Recebimento + +Mensagens de entrada são tratadas através dos callbacks `onText` e `onBinary`. + +```swift +ws.onText { ws, text in + // String recebida por este WebSocket. + print(text) +} + +ws.onBinary { ws, binary in + // [UInt8] recebido por este WebSocket. + print(binary) +} +``` + +O próprio WebSocket é fornecido como primeiro parâmetro desses callbacks para evitar ciclos de referência. Use esta referência para tomar ação no WebSocket após receber dados. Por exemplo, para enviar uma resposta: + +```swift +// Ecoa mensagens recebidas. +ws.onText { ws, text in + ws.send(text) +} +``` + +## Fechamento + +Para fechar um WebSocket, chame o método `close`. + +```swift +ws.close() +``` + +Este método retorna um future que será completado quando o WebSocket for fechado. Como `send`, você também pode passar uma promise para este método. + +```swift +ws.close(promise: nil) +``` + +Ou usar `await` se estiver usando `async`/`await`: + +```swift +try await ws.close() +``` + +Para ser notificado quando o peer fechar a conexão, use `onClose`. Este future será completado quando o cliente ou o servidor fechar o WebSocket. + +```swift +ws.onClose.whenComplete { result in + // Fechou com sucesso ou falhou. +} +``` + +A propriedade `closeCode` é definida quando o WebSocket fecha. Pode ser usada para determinar por que o peer fechou a conexão. + +## Ping / Pong + +Mensagens de ping e pong são enviadas automaticamente pelo cliente e servidor para manter conexões WebSocket ativas. Sua aplicação pode ouvir esses eventos usando os callbacks `onPing` e `onPong`. + +```swift +ws.onPing { ws in + // Ping foi recebido. +} + +ws.onPong { ws in + // Pong foi recebido. +} +``` diff --git a/docs/basics/async.pt.md b/docs/basics/async.pt.md new file mode 100644 index 000000000..1a2caa90f --- /dev/null +++ b/docs/basics/async.pt.md @@ -0,0 +1,443 @@ +# Async + +## Async Await + +O Swift 5.5 introduziu concorrência na linguagem na forma de `async`/`await`. Isso fornece uma maneira de primeira classe de lidar com código assíncrono em aplicações Swift e Vapor. + +O Vapor é construído sobre o [SwiftNIO](https://github.com/apple/swift-nio.git), que fornece tipos primitivos para programação assíncrona de baixo nível. Estes foram (e ainda são) usados em todo o Vapor antes da chegada do `async`/`await`. No entanto, a maior parte do código de aplicação agora pode ser escrita usando `async`/`await` em vez de usar `EventLoopFuture`s. Isso simplificará seu código e tornará muito mais fácil raciocinar sobre ele. + +A maioria das APIs do Vapor agora oferece tanto versões `EventLoopFuture` quanto `async`/`await` para você escolher qual é melhor. Em geral, você deve usar apenas um modelo de programação por handler de rota e não misturar no seu código. Para aplicações que precisam de controle explícito sobre event loops, ou aplicações de alto desempenho, você deve continuar usando `EventLoopFuture`s até que executores personalizados sejam implementados. Para todos os outros, você deve usar `async`/`await`, pois os benefícios de legibilidade e manutenibilidade superam em muito qualquer pequena penalidade de desempenho. + +### Migrando para async/await + +Existem alguns passos necessários para migrar para async/await. Para começar, se estiver usando macOS, você deve estar no macOS 12 Monterey ou superior e Xcode 13.1 ou superior. Para outras plataformas, você precisa estar executando Swift 5.5 ou superior. Em seguida, certifique-se de que atualizou todas as suas dependências. + +No seu Package.swift, defina a versão de tools para 5.5 no topo do arquivo: + +```swift +// swift-tools-version:5.5 +import PackageDescription + +// ... +``` + +Em seguida, defina a versão da plataforma para macOS 12: + +```swift + platforms: [ + .macOS(.v12) + ], +``` + +Por fim, atualize o target `Run` para marcá-lo como um target executável: + +```swift +.executableTarget(name: "Run", dependencies: [.target(name: "App")]), +``` + +Nota: se você está fazendo deploy no Linux, certifique-se de atualizar a versão do Swift lá também, ex: no Heroku ou no seu Dockerfile. Por exemplo, seu Dockerfile mudaria para: + +```diff +-FROM swift:5.2-focal as build ++FROM swift:5.5-focal as build +... +-FROM swift:5.2-focal-slim ++FROM swift:5.5-focal-slim +``` + +Agora você pode migrar o código existente. Geralmente, funções que retornam `EventLoopFuture`s agora são `async`. Por exemplo: + +```swift +routes.get("firstUser") { req -> EventLoopFuture in + User.query(on: req.db).first().unwrap(or: Abort(.notFound)).flatMap { user in + user.lastAccessed = Date() + return user.update(on: req.db).map { + return user.name + } + } +} +``` + +Agora se torna: + +```swift +routes.get("firstUser") { req async throws -> String in + guard let user = try await User.query(on: req.db).first() else { + throw Abort(.notFound) + } + user.lastAccessed = Date() + try await user.update(on: req.db) + return user.name +} +``` + +### Trabalhando com APIs antigas e novas + +Se você encontrar APIs que ainda não oferecem uma versão `async`/`await`, pode chamar `.get()` em uma função que retorna um `EventLoopFuture` para convertê-lo. + +Ex. + +```swift +return someMethodCallThatReturnsAFuture().flatMap { futureResult in + // usa futureResult +} +``` + +Pode se tornar + +```swift +let futureResult = try await someMethodThatReturnsAFuture().get() +``` + +Se você precisar ir na direção contrária, pode converter + +```swift +let myString = try await someAsyncFunctionThatGetsAString() +``` + +para + +```swift +let promise = request.eventLoop.makePromise(of: String.self) +promise.completeWithTask { + try await someAsyncFunctionThatGetsAString() +} +let futureString: EventLoopFuture = promise.futureResult +``` + +## `EventLoopFuture`s + +Você pode ter notado que algumas APIs no Vapor esperam ou retornam um tipo genérico `EventLoopFuture`. Se esta é a primeira vez que você ouve sobre futures, eles podem parecer um pouco confusos no início. Mas não se preocupe, este guia mostrará como aproveitar suas poderosas APIs. + +Promises e futures são tipos relacionados, mas distintos. Promises são usadas para _criar_ futures. Na maior parte do tempo, você estará trabalhando com futures retornados pelas APIs do Vapor e não precisará se preocupar em criar promises. + +|tipo|descrição|mutabilidade| +|-|-|-| +|`EventLoopFuture`|Referência a um valor que pode não estar disponível ainda.|somente leitura| +|`EventLoopPromise`|Uma promessa de fornecer algum valor assincronamente.|leitura/escrita| + +Futures são uma alternativa a APIs assíncronas baseadas em callbacks. Futures podem ser encadeados e transformados de maneiras que simples closures não conseguem. + +## Transformando + +Assim como optionals e arrays no Swift, futures podem ser mapeados e flat-mapped. Estas são as operações mais comuns que você realizará em futures. + +|método|argumento|descrição| +|-|-|-| +|[`map`](#map)|`(T) -> U`|Mapeia um valor de future para um valor diferente.| +|[`flatMapThrowing`](#flatmapthrowing)|`(T) throws -> U`|Mapeia um valor de future para um valor diferente ou um erro.| +|[`flatMap`](#flatmap)|`(T) -> EventLoopFuture`|Mapeia um valor de future para um valor de _future_ diferente.| +|[`transform`](#transform)|`U`|Mapeia um future para um valor já disponível.| + +Se você observar as assinaturas dos métodos `map` e `flatMap` em `Optional` e `Array`, verá que são muito similares aos métodos disponíveis em `EventLoopFuture`. + +### map + +O método `map` permite que você transforme o valor do future em outro valor. Como o valor do future pode não estar disponível ainda (pode ser o resultado de uma tarefa assíncrona), precisamos fornecer uma closure para aceitar o valor. + +```swift +/// Assuma que recebemos uma string futura de alguma API +let futureString: EventLoopFuture = ... + +/// Mapeia a string futura para um inteiro +let futureInt = futureString.map { string in + print(string) // A String real + return Int(string) ?? 0 +} + +/// Agora temos um inteiro futuro +print(futureInt) // EventLoopFuture +``` + +### flatMapThrowing + +O método `flatMapThrowing` permite que você transforme o valor do future em outro valor _ou_ lance um erro. + +!!! info + Como lançar um erro deve criar um novo future internamente, este método é prefixado com `flatMap` mesmo que a closure não aceite um retorno de future. + +```swift +/// Assuma que recebemos uma string futura de alguma API +let futureString: EventLoopFuture = ... + +/// Mapeia a string futura para um inteiro +let futureInt = futureString.flatMapThrowing { string in + print(string) // A String real + // Converte a string para um inteiro ou lança um erro + guard let int = Int(string) else { + throw Abort(...) + } + return int +} + +/// Agora temos um inteiro futuro +print(futureInt) // EventLoopFuture +``` + +### flatMap + +O método `flatMap` permite que você transforme o valor do future em outro valor de future. Ele recebe o nome "flat" map porque é o que permite evitar a criação de futures aninhados (ex: `EventLoopFuture>`). Em outras palavras, ele ajuda a manter seus generics planos. + +```swift +/// Assuma que recebemos uma string futura de alguma API +let futureString: EventLoopFuture = ... + +/// Assuma que criamos um client HTTP +let client: Client = ... + +/// flatMap da string futura para uma resposta futura +let futureResponse = futureString.flatMap { string in + client.get(string) // EventLoopFuture +} + +/// Agora temos uma resposta futura +print(futureResponse) // EventLoopFuture +``` + +!!! info + Se ao invés disso usássemos `map` no exemplo acima, teríamos acabado com: `EventLoopFuture>`. + +Para chamar um método que lança erro dentro de um `flatMap`, use as palavras-chave `do` / `catch` do Swift e crie um [future completado](#makefuture). + +```swift +/// Assuma a string futura e o client do exemplo anterior. +let futureResponse = futureString.flatMap { string in + let url: URL + do { + // Algum método síncrono que lança erro. + url = try convertToURL(string) + } catch { + // Usa o event loop para criar um future pré-completado. + return eventLoop.makeFailedFuture(error) + } + return client.get(url) // EventLoopFuture +} +``` + +### transform + +O método `transform` permite que você modifique o valor de um future, ignorando o valor existente. Isso é especialmente útil para transformar os resultados de `EventLoopFuture` onde o valor real do future não é importante. + +!!! tip + `EventLoopFuture`, às vezes chamado de sinal, é um future cujo único propósito é notificá-lo sobre a conclusão ou falha de alguma operação assíncrona. + +```swift +/// Assuma que recebemos um future void de alguma API +let userDidSave: EventLoopFuture = ... + +/// Transforma o future void em um status HTTP +let futureStatus = userDidSave.transform(to: HTTPStatus.ok) +print(futureStatus) // EventLoopFuture +``` + +Mesmo que tenhamos fornecido um valor já disponível para `transform`, isso ainda é uma _transformação_. O future não será completado até que todos os futures anteriores tenham sido completados (ou falhado). + +### Encadeamento + +A grande vantagem das transformações em futures é que elas podem ser encadeadas. Isso permite que você expresse muitas conversões e subtarefas facilmente. + +Vamos modificar os exemplos acima para ver como podemos aproveitar o encadeamento. + +```swift +/// Assuma que recebemos uma string futura de alguma API +let futureString: EventLoopFuture = ... + +/// Assuma que criamos um client HTTP +let client: Client = ... + +/// Transforma a string em uma url, depois em uma resposta +let futureResponse = futureString.flatMapThrowing { string in + guard let url = URL(string: string) else { + throw Abort(.badRequest, reason: "Invalid URL string: \(string)") + } + return url +}.flatMap { url in + client.get(url) +} + +print(futureResponse) // EventLoopFuture +``` + +Após a chamada inicial ao map, um `EventLoopFuture` temporário é criado. Este future é então imediatamente flat-mapped para um `EventLoopFuture`. + +## Future + +Vamos dar uma olhada em alguns outros métodos para usar `EventLoopFuture`. + +### makeFuture + +Você pode usar um event loop para criar futures pré-completados com o valor ou um erro. + +```swift +// Cria um future pré-sucedido. +let futureString: EventLoopFuture = eventLoop.makeSucceededFuture("hello") + +// Cria um future pré-falhado. +let futureString: EventLoopFuture = eventLoop.makeFailedFuture(error) +``` + +### whenComplete + +Você pode usar `whenComplete` para adicionar um callback que será executado quando o future for bem-sucedido ou falhar. + +```swift +/// Assuma que recebemos uma string futura de alguma API +let futureString: EventLoopFuture = ... + +futureString.whenComplete { result in + switch result { + case .success(let string): + print(string) // A String real + case .failure(let error): + print(error) // Um Error do Swift + } +} +``` + +!!! note + Você pode adicionar quantos callbacks quiser a um future. + +### Get + +No caso de não haver uma alternativa baseada em concorrência para uma API, você pode aguardar o valor do future usando `try await future.get()`. + +```swift +/// Assuma que recebemos uma string futura de alguma API +let futureString: EventLoopFuture = ... + +/// Aguarda a string estar pronta +let string: String = try await futureString.get() +print(string) /// String +``` + +### Wait + +!!! warning + A função `wait()` está obsoleta, veja [`Get`](#get) para a abordagem recomendada. + +Você pode usar `.wait()` para aguardar sincronamente até que o future seja completado. Como um future pode falhar, esta chamada pode lançar erros. + +```swift +/// Assuma que recebemos uma string futura de alguma API +let futureString: EventLoopFuture = ... + +/// Bloqueia até a string estar pronta +let string = try futureString.wait() +print(string) /// String +``` + +`wait()` só pode ser usado em uma thread de background ou na thread principal, ou seja, em `configure.swift`. Ele _não_ pode ser usado em uma thread de event loop, ou seja, em closures de rota. + +!!! warning + Tentar chamar `wait()` em uma thread de event loop causará uma falha de asserção. + +## Promise + +Na maior parte do tempo, você estará transformando futures retornados por chamadas às APIs do Vapor. No entanto, em algum momento você pode precisar criar uma promise por conta própria. + +Para criar uma promise, você precisará de acesso a um `EventLoop`. Você pode obter acesso a um event loop de `Application` ou `Request` dependendo do contexto. + +```swift +let eventLoop: EventLoop + +// Cria uma nova promise para alguma string. +let promiseString = eventLoop.makePromise(of: String.self) +print(promiseString) // EventLoopPromise +print(promiseString.futureResult) // EventLoopFuture + +// Completa o future associado. +promiseString.succeed("Hello") + +// Falha o future associado. +promiseString.fail(...) +``` + +!!! info + Uma promise só pode ser completada uma vez. Quaisquer completações subsequentes serão ignoradas. + +Promises podem ser completadas (`succeed` / `fail`) de qualquer thread. É por isso que promises requerem um event loop para serem inicializadas. Promises garantem que a ação de completação seja retornada ao seu event loop para execução. + +## Event Loop + +Quando sua aplicação inicia, ela geralmente criará um event loop para cada core da CPU em que está sendo executada. Cada event loop possui exatamente uma thread. Se você está familiarizado com event loops do Node.js, os do Vapor são similares. A principal diferença é que o Vapor pode executar múltiplos event loops em um único processo, já que o Swift suporta multi-threading. + +Cada vez que um client se conecta ao seu servidor, ele será atribuído a um dos event loops. Desse ponto em diante, toda a comunicação entre o servidor e aquele client acontecerá no mesmo event loop (e por associação, na thread daquele event loop). + +O event loop é responsável por manter o controle do estado de cada client conectado. Se houver uma requisição do client esperando para ser lida, o event loop dispara uma notificação de leitura, fazendo com que os dados sejam lidos. Uma vez que toda a requisição é lida, quaisquer futures aguardando os dados daquela requisição serão completados. + +Em closures de rota, você pode acessar o event loop atual via `Request`. + +```swift +req.eventLoop.makePromise(of: ...) +``` + +!!! warning + O Vapor espera que closures de rota permaneçam no `req.eventLoop`. Se você trocar de threads, deve garantir que o acesso à `Request` e o future de resposta final aconteçam no event loop da requisição. + +Fora de closures de rota, você pode obter um dos event loops disponíveis via `Application`. + +```swift +app.eventLoopGroup.next().makePromise(of: ...) +``` + +### hop + +Você pode alterar o event loop de um future usando `hop`. + +```swift +futureString.hop(to: otherEventLoop) +``` + +## Bloqueio + +Chamar código bloqueante em uma thread de event loop pode impedir que sua aplicação responda a requisições recebidas em tempo hábil. Um exemplo de chamada bloqueante seria algo como `libc.sleep(_:)`. + +```swift +app.get("hello") { req in + /// Coloca a thread do event loop para dormir. + sleep(5) + + /// Retorna uma string simples quando a thread desperta. + return "Olá, mundo!" +} +``` + +`sleep(_:)` é um comando que bloqueia a thread atual pelo número de segundos fornecido. Se você fizer trabalho bloqueante como este diretamente em um event loop, o event loop será incapaz de responder a quaisquer outros clients atribuídos a ele durante a duração do trabalho bloqueante. Em outras palavras, se você fizer `sleep(5)` em um event loop, todos os outros clients conectados àquele event loop (possivelmente centenas ou milhares) serão atrasados por pelo menos 5 segundos. + +Certifique-se de executar qualquer trabalho bloqueante em background. Use promises para notificar o event loop quando este trabalho for concluído de forma não-bloqueante. + +```swift +app.get("hello") { req -> EventLoopFuture in + /// Despacha algum trabalho para acontecer em uma thread de background + return req.application.threadPool.runIfActive(eventLoop: req.eventLoop) { + /// Coloca a thread de background para dormir + /// Isso não afetará nenhum dos event loops + sleep(5) + + /// Quando o "trabalho bloqueante" for concluído, + /// retorna o resultado. + return "Olá, mundo!" + } +} +``` + +Nem todas as chamadas bloqueantes serão tão óbvias quanto `sleep(_:)`. Se você suspeita que uma chamada que está usando pode ser bloqueante, pesquise sobre o método em si ou pergunte a alguém. As seções abaixo detalham como métodos podem bloquear. + +### I/O Bound + +Bloqueio I/O bound significa aguardar um recurso lento como uma rede ou disco rígido, que podem ser ordens de magnitude mais lentos que a CPU. Bloquear a CPU enquanto você espera por esses recursos resulta em tempo desperdiçado. + +!!! danger + Nunca faça chamadas bloqueantes I/O bound diretamente em um event loop. + +Todos os pacotes do Vapor são construídos sobre SwiftNIO e usam I/O não-bloqueante. No entanto, existem muitos pacotes Swift e bibliotecas C por aí que usam I/O bloqueante. As chances são de que, se uma função está fazendo I/O de disco ou rede e usa uma API síncrona (sem callbacks ou futures), ela é bloqueante. + +### CPU Bound + +A maior parte do tempo durante uma requisição é gasta aguardando recursos externos como consultas ao banco de dados e requisições de rede serem carregadas. Como o Vapor e o SwiftNIO são não-bloqueantes, esse tempo ocioso pode ser usado para atender outras requisições recebidas. No entanto, algumas rotas na sua aplicação podem precisar fazer trabalho pesado vinculado à CPU como resultado de uma requisição. + +Enquanto um event loop está processando trabalho vinculado à CPU, ele será incapaz de responder a outras requisições recebidas. Isso normalmente está ok, pois CPUs são rápidas e a maioria do trabalho de CPU que aplicações web fazem é leve. Mas isso pode se tornar um problema se rotas com trabalho de CPU de longa duração estão impedindo que requisições para rotas mais rápidas sejam respondidas rapidamente. + +Identificar trabalho de CPU de longa duração na sua aplicação e movê-lo para threads de background pode ajudar a melhorar a confiabilidade e responsividade do seu serviço. Trabalho vinculado à CPU é mais uma área cinzenta do que trabalho I/O bound, e cabe a você determinar onde quer traçar a linha. + +Um exemplo comum de trabalho pesado vinculado à CPU é o hashing Bcrypt durante o cadastro e login de usuários. O Bcrypt é deliberadamente muito lento e intensivo em CPU por razões de segurança. Este pode ser o trabalho mais intensivo em CPU que uma aplicação web simples realmente faz. Mover o hashing para uma thread de background pode permitir que a CPU intercale trabalho do event loop enquanto calcula hashes, resultando em maior concorrência. diff --git a/docs/basics/client.pt.md b/docs/basics/client.pt.md new file mode 100644 index 000000000..0f4cefe73 --- /dev/null +++ b/docs/basics/client.pt.md @@ -0,0 +1,75 @@ +# Cliente + +A API de client do Vapor permite que você faça chamadas HTTP para recursos externos. Ela é construída sobre o [async-http-client](https://github.com/swift-server/async-http-client) e integra com a API de [conteúdo](content.md). + +## Visão Geral + +Você pode obter acesso ao client padrão via `Application` ou em um handler de rota via `Request`. + +```swift +app.client // Client + +app.get("test") { req in + req.client // Client +} +``` + +O client da aplicação é útil para fazer requisições HTTP durante o tempo de configuração. Se você estiver fazendo requisições HTTP em um handler de rota, sempre use o client da requisição. + +### Métodos + +Para fazer uma requisição `GET`, passe a URL desejada para o método de conveniência `get`. + +```swift +let response = try await req.client.get("https://httpbin.org/status/200") +``` + +Existem métodos para cada um dos verbos HTTP como `get`, `post` e `delete`. A resposta do client é retornada como um future e contém o status HTTP, headers e body. + +### Conteúdo + +A API de [conteúdo](content.md) do Vapor está disponível para manipular dados em requisições e respostas do client. Para codificar conteúdo, parâmetros de query ou adicionar headers à requisição, use a closure `beforeSend`. + +```swift +let response = try await req.client.post("https://httpbin.org/status/200") { req in + // Codifica a query string na URL da requisição. + try req.query.encode(["q": "test"]) + + // Codifica JSON no body da requisição. + try req.content.encode(["hello": "world"]) + + // Adiciona header de auth à requisição + let auth = BasicAuthorization(username: "something", password: "somethingelse") + req.headers.basicAuthorization = auth +} +// Manipula a resposta. +``` + +Você também pode decodificar o body da resposta usando `Content` de maneira similar: + +```swift +let response = try await req.client.get("https://httpbin.org/json") +let json = try response.content.decode(MyJSONResponse.self) +``` + +Se você estiver usando futures, pode usar `flatMapThrowing`: + +```swift +return req.client.get("https://httpbin.org/json").flatMapThrowing { res in + try res.content.decode(MyJSONResponse.self) +}.flatMap { json in + // Use o JSON aqui +} +``` + +## Configuração + +Você pode configurar o client HTTP subjacente através da aplicação. + +```swift +// Desabilita o redirecionamento automático. +app.http.client.configuration.redirectConfiguration = .disallow +``` + +Note que você deve configurar o client padrão _antes_ de usá-lo pela primeira vez. + diff --git a/docs/basics/content.pt.md b/docs/basics/content.pt.md new file mode 100644 index 000000000..07ffe9db1 --- /dev/null +++ b/docs/basics/content.pt.md @@ -0,0 +1,282 @@ +# Conteúdo + +A API de conteúdo do Vapor permite que você codifique / decodifique facilmente structs Codable de / para mensagens HTTP. A codificação [JSON](https://tools.ietf.org/html/rfc7159) é usada por padrão com suporte nativo para [URL-Encoded Form](https://en.wikipedia.org/wiki/Percent-encoding#The_application/x-www-form-urlencoded_type) e [Multipart](https://tools.ietf.org/html/rfc2388). A API também é configurável, permitindo que você adicione, modifique ou substitua estratégias de codificação para certos tipos de conteúdo HTTP. + +## Visão Geral + +Para entender como a API de conteúdo do Vapor funciona, você deve primeiro entender alguns conceitos básicos sobre mensagens HTTP. Veja o seguinte exemplo de requisição. + +```http +POST /greeting HTTP/1.1 +content-type: application/json +content-length: 18 + +{"hello": "world"} +``` + +Esta requisição indica que contém dados codificados em JSON usando o header `content-type` e o media type `application/json`. Como prometido, alguns dados JSON seguem após os headers no body. + +### Content Struct + +O primeiro passo para decodificar esta mensagem HTTP é criar um tipo Codable que corresponda à estrutura esperada. + +```swift +struct Greeting: Content { + var hello: String +} +``` + +Conformar o tipo a `Content` adicionará automaticamente conformidade a `Codable` junto com utilitários adicionais para trabalhar com a API de conteúdo. + +Uma vez que você tenha a estrutura de conteúdo, pode decodificá-la da requisição recebida usando `req.content`. + +```swift +app.post("greeting") { req in + let greeting = try req.content.decode(Greeting.self) + print(greeting.hello) // "world" + return HTTPStatus.ok +} +``` + +O método decode usa o tipo de conteúdo da requisição para encontrar um decoder apropriado. Se nenhum decoder for encontrado, ou a requisição não contiver o header de tipo de conteúdo, um erro `415` será lançado. + +Isso significa que esta rota aceita automaticamente todos os outros tipos de conteúdo suportados, como url-encoded form: + +```http +POST /greeting HTTP/1.1 +content-type: application/x-www-form-urlencoded +content-length: 11 + +hello=world +``` + +No caso de upload de arquivos, sua propriedade de conteúdo deve ser do tipo `Data` + +```swift +struct Profile: Content { + var name: String + var email: String + var image: Data +} +``` + +### Tipos de Media Suportados + +Abaixo estão os tipos de media que a API de conteúdo suporta por padrão. + +|nome|valor do header|media type| +|-|-|-| +|JSON|application/json|`.json`| +|Multipart|multipart/form-data|`.formData`| +|URL-Encoded Form|application/x-www-form-urlencoded|`.urlEncodedForm`| +|Plaintext|text/plain|`.plainText`| +|HTML|text/html|`.html`| + +Nem todos os tipos de media suportam todos os recursos do `Codable`. Por exemplo, JSON não suporta fragmentos de nível superior e Plaintext não suporta dados aninhados. + +## Query + +As APIs de conteúdo do Vapor suportam a manipulação de dados codificados em URL na query string da URL. + +### Decodificação + +Para entender como funciona a decodificação de uma query string de URL, veja o seguinte exemplo de requisição. + +```http +GET /hello?name=Vapor HTTP/1.1 +content-length: 0 +``` + +Assim como as APIs para manipulação de conteúdo do body de mensagens HTTP, o primeiro passo para analisar query strings de URL é criar uma `struct` que corresponda à estrutura esperada. + +```swift +struct Hello: Content { + var name: String? +} +``` + +Note que `name` é uma `String` opcional, já que query strings de URL devem ser sempre opcionais. Se você quiser exigir um parâmetro, use um parâmetro de rota. + +Agora que você tem uma struct `Content` para a query string esperada desta rota, pode decodificá-la. + +```swift +app.get("hello") { req -> String in + let hello = try req.query.decode(Hello.self) + return "Olá, \(hello.name ?? "Anônimo")" +} +``` + +Esta rota resultaria na seguinte resposta dado o exemplo de requisição acima: + +```http +HTTP/1.1 200 OK +content-length: 11 + +Olá, Vapor +``` + +Se a query string fosse omitida, como na seguinte requisição, o nome "Anônimo" seria usado. + +```http +GET /hello HTTP/1.1 +content-length: 0 +``` + +### Valor Único + +Além de decodificar para uma struct `Content`, o Vapor também suporta buscar valores individuais da query string usando subscripts. + +```swift +let name: String? = req.query["name"] +``` + +## Hooks + +O Vapor chamará automaticamente `beforeEncode` e `afterDecode` em um tipo `Content`. Implementações padrão são fornecidas e não fazem nada, mas você pode usar esses métodos para executar lógica personalizada. + +```swift +// Executa após este Content ser decodificado. `mutating` é necessário apenas para structs, não para classes. +mutating func afterDecode() throws { + // O nome pode não ser enviado, mas se for, não pode ser uma string vazia. + self.name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines) + if let name = self.name, name.isEmpty { + throw Abort(.badRequest, reason: "Name must not be empty.") + } +} + +// Executa antes deste Content ser codificado. `mutating` é necessário apenas para structs, não para classes. +mutating func beforeEncode() throws { + // Deve *sempre* retornar um nome, e não pode ser uma string vazia. + guard + let name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines), + !name.isEmpty + else { + throw Abort(.badRequest, reason: "Name must not be empty.") + } + self.name = name +} +``` + +## Substituindo Padrões + +Os encoders e decoders padrão usados pelas APIs de conteúdo do Vapor podem ser configurados. + +### Global + +`ContentConfiguration.global` permite que você altere os encoders e decoders que o Vapor usa por padrão. Isso é útil para mudar como toda a sua aplicação analisa e serializa dados. + +```swift +// cria um novo encoder JSON que usa datas em unix-timestamp +let encoder = JSONEncoder() +encoder.dateEncodingStrategy = .secondsSince1970 + +// substitui o encoder global usado para o media type `.json` +ContentConfiguration.global.use(encoder: encoder, for: .json) +``` + +A mutação de `ContentConfiguration` é geralmente feita em `configure.swift`. + +### Uso Pontual + +Chamadas a métodos de codificação e decodificação como `req.content.decode` suportam a passagem de coders personalizados para usos pontuais. + +```swift +// cria um novo decoder JSON que usa datas em unix-timestamp +let decoder = JSONDecoder() +decoder.dateDecodingStrategy = .secondsSince1970 + +// decodifica a struct Hello usando um decoder personalizado +let hello = try req.content.decode(Hello.self, using: decoder) +``` + +## Coders Personalizados + +Aplicações e pacotes de terceiros podem adicionar suporte para tipos de media que o Vapor não suporta por padrão criando coders personalizados. + +### Content + +O Vapor especifica dois protocolos para coders capazes de manipular conteúdo em bodies de mensagens HTTP: `ContentDecoder` e `ContentEncoder`. + +```swift +public protocol ContentEncoder { + func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws + where E: Encodable +} + +public protocol ContentDecoder { + func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D + where D: Decodable +} +``` + +Conformar a esses protocolos permite que seus coders personalizados sejam registrados em `ContentConfiguration` conforme especificado acima. + +### URL Query + +O Vapor especifica dois protocolos para coders capazes de manipular conteúdo em query strings de URL: `URLQueryDecoder` e `URLQueryEncoder`. + +```swift +public protocol URLQueryDecoder { + func decode(_ decodable: D.Type, from url: URI) throws -> D + where D: Decodable +} + +public protocol URLQueryEncoder { + func encode(_ encodable: E, to url: inout URI) throws + where E: Encodable +} +``` + +Conformar a esses protocolos permite que seus coders personalizados sejam registrados em `ContentConfiguration` para manipulação de query strings de URL usando os métodos `use(urlEncoder:)` e `use(urlDecoder:)`. + +### `ResponseEncodable` Personalizado + +Outra abordagem envolve implementar `ResponseEncodable` em seus tipos. Considere este tipo wrapper `HTML` trivial: + +```swift +struct HTML { + let value: String +} +``` + +Em seguida, sua implementação de `ResponseEncodable` ficaria assim: + +```swift +extension HTML: ResponseEncodable { + public func encodeResponse(for request: Request) -> EventLoopFuture { + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "text/html") + return request.eventLoop.makeSucceededFuture(.init( + status: .ok, headers: headers, body: .init(string: value) + )) + } +} +``` + +Se você estiver usando `async`/`await`, pode usar `AsyncResponseEncodable`: + +```swift +extension HTML: AsyncResponseEncodable { + public func encodeResponse(for request: Request) async throws -> Response { + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "text/html") + return .init(status: .ok, headers: headers, body: .init(string: value)) + } +} +``` + +Note que isso permite personalizar o header `Content-Type`. Veja a [referência de `HTTPHeaders`](https://api.vapor.codes/vapor/documentation/vapor/response/headers) para mais detalhes. + +Você pode então usar `HTML` como um tipo de resposta nas suas rotas: + +```swift +app.get { _ in + HTML(value: """ + + +

Olá, Mundo!

+ + + """) +} +``` diff --git a/docs/basics/controllers.pt.md b/docs/basics/controllers.pt.md new file mode 100644 index 000000000..f2673cfaf --- /dev/null +++ b/docs/basics/controllers.pt.md @@ -0,0 +1,71 @@ +# Controladores + +Controllers são uma ótima maneira de organizar seu código. Eles são coleções de métodos que aceitam uma requisição e retornam uma resposta. + +Um bom lugar para colocar seus controllers é na pasta [Controllers](../getting-started/folder-structure.md#controllers). + +## Visão Geral + +Vamos dar uma olhada em um exemplo de controller. + +```swift +import Vapor + +struct TodosController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + let todos = routes.grouped("todos") + todos.get(use: index) + todos.post(use: create) + + todos.group(":id") { todo in + todo.get(use: show) + todo.put(use: update) + todo.delete(use: delete) + } + } + + func index(req: Request) async throws -> [Todo] { + try await Todo.query(on: req.db).all() + } + + func create(req: Request) async throws -> Todo { + let todo = try req.content.decode(Todo.self) + try await todo.save(on: req.db) + return todo + } + + func show(req: Request) async throws -> Todo { + guard let todo = try await Todo.find(req.parameters.get("id"), on: req.db) else { + throw Abort(.notFound) + } + return todo + } + + func update(req: Request) async throws -> Todo { + guard let todo = try await Todo.find(req.parameters.get("id"), on: req.db) else { + throw Abort(.notFound) + } + let updatedTodo = try req.content.decode(Todo.self) + todo.title = updatedTodo.title + try await todo.save(on: req.db) + return todo + } + + func delete(req: Request) async throws -> HTTPStatus { + guard let todo = try await Todo.find(req.parameters.get("id"), on: req.db) else { + throw Abort(.notFound) + } + try await todo.delete(on: req.db) + return .ok + } +} +``` + +Os métodos do controller devem sempre aceitar uma `Request` e retornar algo que seja `ResponseEncodable`. Este método pode ser assíncrono ou síncrono. + + +Por fim, você precisa registrar o controller em `routes.swift`: + +```swift +try app.register(collection: TodosController()) +``` diff --git a/docs/basics/environment.pt.md b/docs/basics/environment.pt.md new file mode 100644 index 000000000..66b625a0f --- /dev/null +++ b/docs/basics/environment.pt.md @@ -0,0 +1,144 @@ +# Ambiente + +A API de Environment do Vapor ajuda você a configurar seu app dinamicamente. Por padrão, seu app usará o ambiente `development`. Você pode definir outros ambientes úteis como `production` ou `staging` e alterar como seu app é configurado em cada caso. Você também pode carregar variáveis do ambiente do processo ou de arquivos `.env` (dotenv) conforme suas necessidades. + +Para acessar o ambiente atual, use `app.environment`. Você pode usar um switch nesta propriedade em `configure(_:)` para executar lógica de configuração diferente. + +```swift +switch app.environment { +case .production: + app.databases.use(....) +default: + app.databases.use(...) +} +``` + +## Alterando o Ambiente + +Por padrão, seu app será executado no ambiente `development`. Você pode alterar isso passando a flag `--env` (`-e`) durante a inicialização do app. + +```swift +swift run App serve --env production +``` + +O Vapor inclui os seguintes ambientes: + +|nome|abreviação|descrição| +|-|-|-| +|production|prod|Implantado para seus usuários.| +|development|dev|Desenvolvimento local.| +|testing|test|Para testes unitários.| + +!!! info + O ambiente `production` usará por padrão o nível de logging `notice`, a menos que especificado de outra forma. Todos os outros ambientes usam `info` por padrão. + +Você pode passar o nome completo ou a abreviação para a flag `--env` (`-e`). + +```swift +swift run App serve -e prod +``` + +## Variáveis de Processo + +`Environment` oferece uma API simples, baseada em strings, para acessar as variáveis de ambiente do processo. + +```swift +let foo = Environment.get("FOO") +print(foo) // String? +``` + +Além do `get`, `Environment` oferece uma API de busca dinâmica de membros via `process`. + +```swift +let foo = Environment.process.FOO +print(foo) // String? +``` + +Ao executar seu app no terminal, você pode definir variáveis de ambiente usando `export`. + +```sh +export FOO=BAR +swift run App serve +``` + +Ao executar seu app no Xcode, você pode definir variáveis de ambiente editando o scheme `App`. + +## .env (dotenv) + +Arquivos dotenv contêm uma lista de pares chave-valor que são automaticamente carregados no ambiente. Esses arquivos facilitam a configuração de variáveis de ambiente sem precisar defini-las manualmente. + +O Vapor procurará por arquivos dotenv no diretório de trabalho atual. Se você estiver usando o Xcode, certifique-se de definir o diretório de trabalho editando o scheme `App`. + +Considere o seguinte arquivo `.env` colocado na pasta raiz do seu projeto: + +```sh +FOO=BAR +``` + +Quando sua aplicação iniciar, você poderá acessar o conteúdo deste arquivo como outras variáveis de ambiente do processo. + +```swift +let foo = Environment.get("FOO") +print(foo) // String? +``` + +!!! info + Variáveis especificadas em arquivos `.env` não sobrescreverão variáveis que já existam no ambiente do processo. + +Além do `.env`, o Vapor também tentará carregar um arquivo dotenv para o ambiente atual. Por exemplo, quando no ambiente `development`, o Vapor carregará `.env.development`. Quaisquer valores no arquivo de ambiente específico terão precedência sobre o arquivo `.env` geral. + +Um padrão típico é que projetos incluam um arquivo `.env` como template com valores padrão. Arquivos de ambiente específicos são ignorados com o seguinte padrão no `.gitignore`: + +```gitignore +.env.* +``` + +Quando o projeto é clonado em um novo computador, o arquivo template `.env` pode ser copiado e ter os valores corretos inseridos. + +```sh +cp .env .env.development +vim .env.development +``` + +!!! warning + Arquivos dotenv com informações sensíveis como senhas não devem ser commitados no controle de versão. + +Se você estiver tendo dificuldade para carregar arquivos dotenv, tente habilitar o logging de debug com `--log debug` para mais informações. + +## Ambientes Personalizados + +Para definir um nome de ambiente personalizado, estenda `Environment`. + +```swift +extension Environment { + static var staging: Environment { + .custom(name: "staging") + } +} +``` + +O ambiente da aplicação é geralmente definido em `entrypoint.swift` usando `Environment.detect()`. + +```swift +@main +enum Entrypoint { + static func main() async throws { + var env = try Environment.detect() + try LoggingSystem.bootstrap(from: &env) + + let app = Application(env) + defer { app.shutdown() } + + try await configure(app) + try await app.runFromAsyncMainEntrypoint() + } +} +``` + +O método `detect` usa os argumentos de linha de comando do processo e analisa a flag `--env` automaticamente. Você pode substituir esse comportamento inicializando uma struct `Environment` personalizada. + +```swift +let env = Environment(name: "testing", arguments: ["vapor"]) +``` + +O array de argumentos deve conter pelo menos um argumento que represente o nome do executável. Argumentos adicionais podem ser fornecidos para simular a passagem de argumentos via linha de comando. Isso é especialmente útil para testes. diff --git a/docs/basics/errors.pt.md b/docs/basics/errors.pt.md new file mode 100644 index 000000000..644c97724 --- /dev/null +++ b/docs/basics/errors.pt.md @@ -0,0 +1,153 @@ +# Erros + +O Vapor se baseia no protocolo `Error` do Swift para tratamento de erros. Handlers de rota podem tanto lançar (`throw`) um erro quanto retornar um `EventLoopFuture` que falhou. Lançar ou retornar um `Error` do Swift resultará em uma resposta com status `500` e o erro será registrado no log. `AbortError` e `DebuggableError` podem ser usados para alterar a resposta resultante e o logging, respectivamente. O tratamento de erros é feito pelo `ErrorMiddleware`. Este middleware é adicionado à aplicação por padrão e pode ser substituído por lógica personalizada, se desejado. + +## Abort + +O Vapor fornece uma struct de erro padrão chamada `Abort`. Esta struct está em conformidade com `AbortError` e `DebuggableError`. Você pode inicializá-la com um status HTTP e uma razão de falha opcional. + +```swift +// Erro 404, razão padrão "Not Found" usada. +throw Abort(.notFound) + +// Erro 401, razão personalizada usada. +throw Abort(.unauthorized, reason: "Invalid Credentials") +``` + +Em situações assíncronas antigas onde lançar erros não é suportado e você precisa retornar um `EventLoopFuture`, como em uma closure `flatMap`, você pode retornar um future que falhou. + +```swift +guard let user = user else { + req.eventLoop.makeFailedFuture(Abort(.notFound)) +} +return user.save() +``` + +O Vapor inclui uma extensão auxiliar para desempacotar futures com valores opcionais: `unwrap(or:)`. + +```swift +User.find(id, on: db) + .unwrap(or: Abort(.notFound)) + .flatMap +{ user in + // User não-opcional fornecido à closure. +} +``` + +Se `User.find` retornar `nil`, o future falhará com o erro fornecido. Caso contrário, o `flatMap` receberá um valor não-opcional. Se estiver usando `async`/`await`, você pode lidar com opcionais normalmente: + +```swift +guard let user = try await User.find(id, on: db) { + throw Abort(.notFound) +} +``` + + +## Abort Error + +Por padrão, qualquer `Error` do Swift lançado ou retornado por uma closure de rota resultará em uma resposta `500 Internal Server Error`. Quando compilado em modo debug, o `ErrorMiddleware` incluirá uma descrição do erro. Isso é removido por razões de segurança quando o projeto é compilado em modo release. + +Para configurar o status HTTP ou a razão da resposta resultante para um erro específico, faça-o estar em conformidade com `AbortError`. + +```swift +import Vapor + +enum MyError { + case userNotLoggedIn + case invalidEmail(String) +} + +extension MyError: AbortError { + var reason: String { + switch self { + case .userNotLoggedIn: + return "Usuário não está logado." + case .invalidEmail(let email): + return "Endereço de e-mail não é válido: \(email)." + } + } + + var status: HTTPStatus { + switch self { + case .userNotLoggedIn: + return .unauthorized + case .invalidEmail: + return .badRequest + } + } +} +``` + +## Debuggable Error + +O `ErrorMiddleware` usa o método `Logger.report(error:)` para registrar erros lançados pelas suas rotas. Este método verificará a conformidade com protocolos como `CustomStringConvertible` e `LocalizedError` para registrar mensagens legíveis. + +Para personalizar o logging de erros, você pode fazer seus erros estarem em conformidade com `DebuggableError`. Este protocolo inclui várias propriedades úteis como um identificador único, localização no código-fonte e stack trace. A maioria dessas propriedades é opcional, o que torna a adoção da conformidade fácil. + +Para melhor conformidade com `DebuggableError`, seu erro deve ser uma struct para que possa armazenar informações de localização e stack trace, se necessário. Abaixo está um exemplo do enum `MyError` mencionado anteriormente, atualizado para usar uma `struct` e capturar informações de localização do erro. + +```swift +import Vapor + +struct MyError: DebuggableError { + enum Value { + case userNotLoggedIn + case invalidEmail(String) + } + + var identifier: String { + switch self.value { + case .userNotLoggedIn: + return "userNotLoggedIn" + case .invalidEmail: + return "invalidEmail" + } + } + + var reason: String { + switch self.value { + case .userNotLoggedIn: + return "Usuário não está logado." + case .invalidEmail(let email): + return "Endereço de e-mail não é válido: \(email)." + } + } + + var value: Value + var source: ErrorSource? + + init( + _ value: Value, + file: String = #file, + function: String = #function, + line: UInt = #line, + column: UInt = #column + ) { + self.value = value + self.source = .init( + file: file, + function: function, + line: line, + column: column + ) + } +} +``` + +`DebuggableError` possui várias outras propriedades como `possibleCauses` e `suggestedFixes` que você pode usar para melhorar a depurabilidade dos seus erros. Consulte o próprio protocolo para mais informações. + +## Error Middleware + +`ErrorMiddleware` é um dos únicos dois middlewares adicionados à sua aplicação por padrão. Este middleware converte erros do Swift que foram lançados ou retornados pelos seus handlers de rota em respostas HTTP. Sem este middleware, erros lançados resultariam no fechamento da conexão sem uma resposta. + +Para personalizar o tratamento de erros além do que `AbortError` e `DebuggableError` fornecem, você pode substituir o `ErrorMiddleware` pela sua própria lógica de tratamento de erros. Para fazer isso, primeiro remova o middleware de erro padrão inicializando manualmente `app.middleware`. Em seguida, adicione seu próprio middleware de tratamento de erros como o primeiro middleware da sua aplicação. + +```swift +// Remove todos os middlewares padrão (depois, adiciona de volta o logging de rotas) +app.middleware = .init() +app.middleware.use(RouteLoggingMiddleware(logLevel: .info)) +// Adiciona o middleware personalizado de tratamento de erros primeiro. +app.middleware.use(MyErrorMiddleware()) +``` + +Muito poucos middlewares devem ficar _antes_ do middleware de tratamento de erros. Uma exceção notável a esta regra é o `CORSMiddleware`. diff --git a/docs/basics/logging.pt.md b/docs/basics/logging.pt.md new file mode 100644 index 000000000..92fa83beb --- /dev/null +++ b/docs/basics/logging.pt.md @@ -0,0 +1,109 @@ +# Logging + +A API de logging do Vapor é construída sobre o [SwiftLog](https://github.com/apple/swift-log). Isso significa que o Vapor é compatível com todas as [implementações de backend](https://github.com/apple/swift-log#backends) do SwiftLog. + +## Logger + +Instâncias de `Logger` são usadas para emitir mensagens de log. O Vapor fornece algumas maneiras fáceis de obter acesso a um logger. + +### Request + +Cada `Request` recebida possui um logger único que você deve usar para quaisquer logs específicos daquela requisição. + +```swift +app.get("hello") { req -> String in + req.logger.info("Olá, logs!") + return "Olá, mundo!" +} +``` + +O logger da requisição inclui um UUID único identificando a requisição recebida para facilitar o rastreamento de logs. + +``` +[ INFO ] Olá, logs! [request-id: C637065A-8CB0-4502-91DC-9B8615C5D315] (App/routes.swift:10) +``` + +!!! info + Os metadados do logger só serão exibidos no nível de log debug ou inferior. + +### Application + +Para mensagens de log durante a inicialização e configuração do app, use o logger da `Application`. + +```swift +app.logger.info("Setting up migrations...") +app.migrations.use(...) +``` + +### Logger Personalizado + +Em situações onde você não tem acesso a `Application` ou `Request`, você pode inicializar um novo `Logger`. + +```swift +let logger = Logger(label: "dev.logger.my") +logger.info(...) +``` + +Embora loggers personalizados ainda enviem a saída para o backend de logging configurado, eles não terão metadados importantes anexados como o UUID da requisição. Use os loggers específicos da requisição ou da aplicação sempre que possível. + +## Nível + +O SwiftLog suporta vários níveis de logging diferentes. + +|nome|descrição| +|-|-| +|trace|Apropriado para mensagens que contêm informações normalmente úteis apenas ao rastrear a execução de um programa.| +|debug|Apropriado para mensagens que contêm informações normalmente úteis apenas ao depurar um programa.| +|info|Apropriado para mensagens informativas.| +|notice|Apropriado para condições que não são erros, mas que podem exigir tratamento especial.| +|warning|Apropriado para mensagens que não são condições de erro, mas mais severas que notice.| +|error|Apropriado para condições de erro.| +|critical|Apropriado para condições críticas de erro que geralmente requerem atenção imediata.| + +Quando uma mensagem `critical` é registrada, o backend de logging pode realizar operações mais pesadas para capturar o estado do sistema (como capturar stack traces) para facilitar a depuração. + +Por padrão, o Vapor usará o nível de logging `info`. Quando executado com o ambiente `production`, `notice` será usado para melhorar o desempenho. + +### Alterando o Nível de Log + +Independentemente do modo de ambiente, você pode substituir o nível de logging para aumentar ou diminuir a quantidade de logs produzidos. + +O primeiro método é passar a flag opcional `--log` ao iniciar sua aplicação. + +```sh +swift run App serve --log debug +``` + +O segundo método é definir a variável de ambiente `LOG_LEVEL`. + +```sh +export LOG_LEVEL=debug +swift run App serve +``` + +Ambos podem ser feitos no Xcode editando o scheme `App`. + +## Configuração + +O SwiftLog é configurado através do bootstrap do `LoggingSystem` uma vez por processo. Projetos Vapor tipicamente fazem isso em `entrypoint.swift`. + +```swift +var env = try Environment.detect() +try LoggingSystem.bootstrap(from: &env) +``` + +`bootstrap(from:)` é um método auxiliar fornecido pelo Vapor que configurará o handler de log padrão com base nos argumentos de linha de comando e variáveis de ambiente. O handler de log padrão suporta a emissão de mensagens para o terminal com suporte a cores ANSI. + +### Handler Personalizado + +Você pode substituir o handler de log padrão do Vapor e registrar o seu próprio. + +```swift +import Logging + +LoggingSystem.bootstrap { label in + StreamLogHandler.standardOutput(label: label) +} +``` + +Todos os backends suportados pelo SwiftLog funcionarão com o Vapor. No entanto, a alteração do nível de log com argumentos de linha de comando e variáveis de ambiente só é compatível com o handler de log padrão do Vapor. diff --git a/docs/basics/routing.pt.md b/docs/basics/routing.pt.md new file mode 100644 index 000000000..6e20f959d --- /dev/null +++ b/docs/basics/routing.pt.md @@ -0,0 +1,436 @@ +# Rotas + +Roteamento é o processo de encontrar o handler de requisição apropriado para uma requisição recebida. No centro do roteamento do Vapor está um router de alta performance baseado em trie-node do [RoutingKit](https://github.com/vapor/routing-kit). + +## Visão Geral + +Para entender como o roteamento funciona no Vapor, você deve primeiro entender alguns conceitos básicos sobre requisições HTTP. Veja o seguinte exemplo de requisição. + +```http +GET /olá/vapor HTTP/1.1 +host: vapor.codes +content-length: 0 +``` + +Esta é uma requisição HTTP `GET` simples para a URL `/olá/vapor`. Este é o tipo de requisição HTTP que seu navegador faria se você apontasse para a seguinte URL. + +``` +http://vapor.codes/olá/vapor +``` + +### Método HTTP + +A primeira parte da requisição é o método HTTP. `GET` é o método HTTP mais comum, mas existem vários que você usará com frequência. Esses métodos HTTP são frequentemente associados à semântica [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete). + +|Método|CRUD| +|-|-| +|`GET`|Leitura| +|`POST`|Criação| +|`PUT`|Substituição| +|`PATCH`|Atualização| +|`DELETE`|Exclusão| + +### Caminho da Requisição + +Logo após o método HTTP está a URI da requisição. Ela consiste em um caminho começando com `/` e uma query string opcional após `?`. O método HTTP e o caminho são o que o Vapor usa para rotear requisições. + +Após a URI está a versão HTTP seguida por zero ou mais headers e finalmente um body. Como esta é uma requisição `GET`, ela não possui um body. + +### Métodos do Router + +Vamos ver como esta requisição poderia ser tratada no Vapor. + +```swift +app.get("olá", "vapor") { req in + return "Olá, vapor!" +} +``` + +Todos os métodos HTTP comuns estão disponíveis como métodos em `Application`. Eles aceitam um ou mais argumentos de string que representam o caminho da requisição separado por `/`. + +Note que você também poderia escrever isso usando `on` seguido do método. + +```swift +app.on(.GET, "olá", "vapor") { ... } +``` + +Com esta rota registrada, o exemplo de requisição HTTP acima resultará na seguinte resposta HTTP. + +```http +HTTP/1.1 200 OK +content-length: 12 +content-type: text/plain; charset=utf-8 + +Olá, vapor! +``` + +### Parâmetros de Rota + +Agora que roteamos uma requisição com sucesso baseada no método HTTP e caminho, vamos tentar tornar o caminho dinâmico. Observe que o nome "vapor" está codificado tanto no caminho quanto na resposta. Vamos tornar isso dinâmico para que você possa acessar `/hello/` e obter uma resposta. + +```swift +app.get("olá", ":name") { req -> String in + let name = req.parameters.get("name")! + return "Olá, \(name)!" +} +``` + +Usando um componente de caminho prefixado com `:`, indicamos ao router que este é um componente dinâmico. Qualquer string fornecida aqui agora corresponderá a esta rota. Podemos então usar `req.parameters` para acessar o valor da string. + +Se você executar o exemplo de requisição novamente, ainda receberá uma resposta que diz olá para vapor. No entanto, agora você pode incluir qualquer nome após `/olá/` e vê-lo incluído na resposta. Vamos tentar `/olá/swift`. + +```http +GET /olá/swift HTTP/1.1 +content-length: 0 +``` +```http +HTTP/1.1 200 OK +content-length: 12 +content-type: text/plain; charset=utf-8 + +Olá, swift! +``` + +Agora que você entende os conceitos básicos, confira cada seção para aprender mais sobre parâmetros, grupos e mais. + +## Rotas + +Uma rota especifica um handler de requisição para um determinado método HTTP e caminho de URI. Ela também pode armazenar metadados adicionais. + +### Métodos + +Rotas podem ser registradas diretamente na sua `Application` usando vários helpers de métodos HTTP. + +```swift +// corresponde a GET /foo/bar/baz +app.get("foo", "bar", "baz") { req in + ... +} +``` + +Handlers de rota suportam retornar qualquer coisa que seja `ResponseEncodable`. Isso inclui `Content`, uma closure `async` e quaisquer `EventLoopFuture`s onde o valor do future é `ResponseEncodable`. + +Você pode especificar o tipo de retorno de uma rota usando `-> T` antes de `in`. Isso pode ser útil em situações onde o compilador não consegue determinar o tipo de retorno. + +```swift +app.get("foo") { req -> String in + return "bar" +} +``` + +Estes são os métodos helper de rota suportados: + +- `get` +- `post` +- `patch` +- `put` +- `delete` + +Além dos helpers de métodos HTTP, existe uma função `on` que aceita o método HTTP como parâmetro de entrada. + +```swift +// corresponde a OPTIONS /foo/bar/baz +app.on(.OPTIONS, "foo", "bar", "baz") { req in + ... +} +``` + +### Componente de Caminho + +Cada método de registro de rota aceita uma lista variádica de `PathComponent`. Este tipo é expressível por literal de string e tem quatro casos: + +- Constant (`foo`) +- Parameter (`:foo`) +- Anything (`*`) +- Catchall (`**`) + +#### Constant + +Este é um componente de rota estático. Somente requisições com uma string exatamente correspondente nesta posição serão permitidas. + +```swift +// corresponde a GET /foo/bar/baz +app.get("foo", "bar", "baz") { req in + ... +} +``` + +#### Parameter + +Este é um componente de rota dinâmico. Qualquer string nesta posição será permitida. Um componente de caminho parameter é especificado com o prefixo `:`. A string após o `:` será usada como nome do parâmetro. Você pode usar o nome para buscar o valor do parâmetro na requisição posteriormente. + +```swift +// corresponde a GET /foo/bar/baz +// corresponde a GET /foo/qux/baz +// ... +app.get("foo", ":bar", "baz") { req in + ... +} +``` + +#### Anything + +Isso é muito similar ao parameter, exceto que o valor é descartado. Este componente de caminho é especificado apenas como `*`. + +```swift +// corresponde a GET /foo/bar/baz +// corresponde a GET /foo/qux/baz +// ... +app.get("foo", "*", "baz") { req in + ... +} +``` + +#### Catchall + +Este é um componente de rota dinâmico que corresponde a um ou mais componentes. É especificado usando apenas `**`. Qualquer string nesta posição ou posições posteriores será correspondida na requisição. + +```swift +// corresponde a GET /foo/bar +// corresponde a GET /foo/bar/baz +// ... +app.get("foo", "**") { req in + ... +} +``` + +### Parâmetros + +Ao usar um componente de caminho parameter (prefixado com `:`), o valor da URI naquela posição será armazenado em `req.parameters`. Você pode usar o nome do componente de caminho para acessar o valor. + +```swift +// corresponde a GET /olá/foo +// corresponde a GET /olá/bar +// ... +app.get("olá", ":name") { req -> String in + let name = req.parameters.get("name")! + return "Olá, \(name)!" +} +``` + +!!! tip + Podemos ter certeza de que `req.parameters.get` nunca retornará `nil` aqui, pois nosso caminho de rota inclui `:name`. No entanto, se você estiver acessando parâmetros de rota em middleware ou em código acionado por múltiplas rotas, você vai querer lidar com a possibilidade de `nil`. + +!!! tip + Se você quiser recuperar parâmetros de query da URL, ex: `/hello/?name=foo`, você precisa usar as APIs de Conteúdo do Vapor para manipular dados codificados em URL na query string da URL. Veja a [referência de `Content`](content.md) para mais detalhes. + +`req.parameters.get` também suporta a conversão automática do parâmetro para tipos `LosslessStringConvertible`. + +```swift +// corresponde a GET /number/42 +// corresponde a GET /number/1337 +// ... +app.get("number", ":x") { req -> String in + guard let int = req.parameters.get("x", as: Int.self) else { + throw Abort(.badRequest) + } + return "\(int) é um ótimo número" +} +``` + +Os valores da URI correspondidos pelo Catchall (`**`) serão armazenados em `req.parameters` como `[String]`. Você pode usar `req.parameters.getCatchall` para acessar esses componentes. + +```swift +// corresponde a GET /olá/foo +// corresponde a GET /olá/foo/bar +// ... +app.get("olá", "**") { req -> String in + let name = req.parameters.getCatchall().joined(separator: " ") + return "Olá, \(name)!" +} +``` + +### Body Streaming + +Ao registrar uma rota usando o método `on`, você pode especificar como o body da requisição deve ser tratado. Por padrão, bodies de requisição são coletados na memória antes de chamar seu handler. Isso é útil pois permite que a decodificação de conteúdo da requisição seja síncrona, mesmo que sua aplicação leia requisições recebidas de forma assíncrona. + +Por padrão, o Vapor limitará a coleta de body streaming a 16KB de tamanho. Você pode configurar isso usando `app.routes`. + +```swift +// Aumenta o limite de coleta de body streaming para 500kb +app.routes.defaultMaxBodySize = "500kb" +``` + +Se um body streaming sendo coletado exceder o limite configurado, um erro `413 Payload Too Large` será lançado. + +Para configurar a estratégia de coleta de body da requisição para uma rota individual, use o parâmetro `body`. + +```swift +// Coleta bodies streaming (até 1mb de tamanho) antes de chamar esta rota. +app.on(.POST, "listings", body: .collect(maxSize: "1mb")) { req in + // Tratar requisição. +} +``` + +Se um `maxSize` for passado para `collect`, ele substituirá o padrão da aplicação para aquela rota. Para usar o padrão da aplicação, omita o argumento `maxSize`. + +Para requisições grandes, como upload de arquivos, coletar o body da requisição em um buffer pode potencialmente sobrecarregar a memória do seu sistema. Para evitar que o body da requisição seja coletado, use a estratégia `stream`. + +```swift +// O body da requisição não será coletado em um buffer. +app.on(.POST, "upload", body: .stream) { req in + ... +} +``` + +Quando o body da requisição é transmitido via streaming, `req.body.data` será `nil`. Você deve usar `req.body.drain` para manipular cada chunk conforme é enviado para sua rota. + +### Roteamento Case Insensitive + +O comportamento padrão para roteamento é sensível a maiúsculas e minúsculas e preserva o caso. Componentes de caminho `Constant` podem alternativamente ser tratados de forma insensível a maiúsculas e minúsculas e preservando o caso para fins de roteamento; para habilitar este comportamento, configure antes da inicialização da aplicação: +```swift +app.routes.caseInsensitive = true +``` +Nenhuma alteração é feita na requisição de origem; os handlers de rota receberão os componentes do caminho da requisição sem modificação. + + +### Visualizando Rotas + +Você pode acessar as rotas da sua aplicação tornando o serviço `Routes` ou usando `app.routes`. + +```swift +print(app.routes.all) // [Route] +``` + +O Vapor também vem com um comando `routes` que imprime todas as rotas disponíveis em uma tabela formatada em ASCII. + +```sh +$ swift run App routes ++--------+----------------+ +| GET | / | ++--------+----------------+ +| GET | /hello | ++--------+----------------+ +| GET | /todos | ++--------+----------------+ +| POST | /todos | ++--------+----------------+ +| DELETE | /todos/:todoID | ++--------+----------------+ +``` + +### Metadados + +Todos os métodos de registro de rota retornam a `Route` criada. Isso permite que você adicione metadados ao dicionário `userInfo` da rota. Existem alguns métodos padrão disponíveis, como adicionar uma descrição. + +```swift +app.get("hello", ":name") { req in + ... +}.description("diz olá") +``` + +## Grupos de Rotas + +O agrupamento de rotas permite que você crie um conjunto de rotas com um prefixo de caminho ou middleware específico. O agrupamento suporta tanto uma sintaxe de builder quanto de closure. + +Todos os métodos de agrupamento retornam um `RouteBuilder`, o que significa que você pode infinitamente misturar, combinar e aninhar seus grupos com outros métodos de construção de rotas. + +### Prefixo de Caminho + +Grupos de rotas com prefixo de caminho permitem que você adicione um ou mais componentes de caminho a um grupo de rotas. + +```swift +let users = app.grouped("users") +// GET /users +users.get { req in + ... +} +// POST /users +users.post { req in + ... +} +// GET /users/:id +users.get(":id") { req in + let id = req.parameters.get("id")! + ... +} +``` + +Qualquer componente de caminho que você pode passar para métodos como `get` ou `post` pode ser passado para `grouped`. Existe também uma sintaxe alternativa baseada em closure. + +```swift +app.group("users") { users in + // GET /users + users.get { req in + ... + } + // POST /users + users.post { req in + ... + } + // GET /users/:id + users.get(":id") { req in + let id = req.parameters.get("id")! + ... + } +} +``` + +Aninhar grupos de rotas com prefixo de caminho permite que você defina APIs CRUD de forma concisa. + +```swift +app.group("users") { users in + // GET /users + users.get { ... } + // POST /users + users.post { ... } + + users.group(":id") { user in + // GET /users/:id + user.get { ... } + // PATCH /users/:id + user.patch { ... } + // PUT /users/:id + user.put { ... } + } +} +``` + +### Middleware + +Além de prefixar componentes de caminho, você também pode adicionar middleware a grupos de rotas. + +```swift +app.get("fast-thing") { req in + ... +} +app.group(RateLimitMiddleware(requestsPerMinute: 5)) { rateLimited in + rateLimited.get("slow-thing") { req in + ... + } +} +``` + + +Isso é especialmente útil para proteger subconjuntos das suas rotas com diferentes middlewares de autenticação. + +```swift +app.post("login") { ... } +let auth = app.grouped(AuthMiddleware()) +auth.get("dashboard") { ... } +auth.get("logout") { ... } +``` + +## Redirecionamentos + +Redirecionamentos são úteis em vários cenários, como encaminhar localizações antigas para novas para SEO, redirecionar um usuário não autenticado para a página de login ou manter compatibilidade retroativa com a nova versão da sua API. + +Para redirecionar uma requisição, use: + +```swift +req.redirect(to: "/algum/novo/caminho") +``` + +Você também pode especificar o tipo de redirecionamento, por exemplo, para redirecionar uma página permanentemente (para que seu SEO seja atualizado corretamente) use: + +```swift +req.redirect(to: "/algum/novo/caminho", redirectType: .permanent) +``` + +Os diferentes `Redirect`s são: + +* `.permanent` - retorna um redirecionamento **301 Permanent** +* `.normal` - retorna um redirecionamento **303 see other**. Este é o padrão do Vapor e diz ao client para seguir o redirecionamento com uma requisição **GET**. +* `.temporary` - retorna um redirecionamento **307 Temporary**. Isso diz ao client para preservar o método HTTP usado na requisição. + +> Para escolher o código de status de redirecionamento adequado, confira [a lista completa](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection) diff --git a/docs/basics/validation.pt.md b/docs/basics/validation.pt.md new file mode 100644 index 000000000..76842ab8c --- /dev/null +++ b/docs/basics/validation.pt.md @@ -0,0 +1,338 @@ +# Validação + +A API de Validação do Vapor ajuda você a validar o body e os parâmetros de query de uma requisição recebida antes de usar a API de [Conteúdo](content.md) para decodificar dados. + +## Introdução + +A integração profunda do Vapor com o protocolo `Codable` do Swift, que é type-safe, significa que você não precisa se preocupar tanto com validação de dados em comparação com linguagens de tipagem dinâmica. No entanto, ainda existem algumas razões pelas quais você pode querer optar pela validação explícita usando a API de Validação. + +### Erros Legíveis + +Decodificar structs usando a API de [Conteúdo](content.md) produzirá erros se algum dos dados não for válido. No entanto, essas mensagens de erro podem às vezes não ser muito legíveis para humanos. Por exemplo, considere o seguinte enum baseado em string: + +```swift +enum Color: String, Codable { + case red, blue, green +} +``` + +Se um usuário tentar passar a string `"purple"` para uma propriedade do tipo `Color`, ele receberá um erro semelhante ao seguinte: + +``` +Cannot initialize Color from invalid String value purple for key favoriteColor +``` + +Embora este erro esteja tecnicamente correto e tenha protegido o endpoint de um valor inválido com sucesso, ele poderia informar melhor o usuário sobre o erro e quais opções estão disponíveis. Usando a API de Validação, você pode gerar erros como o seguinte: + +``` +favoriteColor is not red, blue, or green +``` + +Além disso, `Codable` parará de tentar decodificar um tipo assim que o primeiro erro for encontrado. Isso significa que mesmo se houver muitas propriedades inválidas na requisição, o usuário verá apenas o primeiro erro. A API de Validação reportará todas as falhas de validação em uma única requisição. + +### Validação Específica + +`Codable` lida bem com validação de tipos, mas às vezes você quer mais do que isso. Por exemplo, validar o conteúdo de uma string ou validar o tamanho de um inteiro. A API de Validação possui validadores para ajudar a validar dados como emails, conjuntos de caracteres, intervalos de inteiros e mais. + +## Validatable + +Para validar uma requisição, você precisará gerar uma coleção de `Validations`. Isso é mais comumente feito conformando um tipo existente a `Validatable`. + +Vamos ver como você poderia adicionar validação a este endpoint simples `POST /users`. Este guia assume que você já está familiarizado com a API de [Conteúdo](content.md). + +```swift +enum Color: String, Codable { + case red, blue, green +} + +struct CreateUser: Content { + var name: String + var username: String + var age: Int + var email: String + var favoriteColor: Color? +} + +app.post("users") { req -> CreateUser in + let user = try req.content.decode(CreateUser.self) + // Faz algo com o usuário. + return user +} +``` + +### Adicionando Validações + +O primeiro passo é conformar o tipo que você está decodificando, neste caso `CreateUser`, a `Validatable`. Isso pode ser feito em uma extensão. + +```swift +extension CreateUser: Validatable { + static func validations(_ validations: inout Validations) { + // As validações vão aqui. + } +} +``` + +O método estático `validations(_:)` será chamado quando `CreateUser` for validado. Quaisquer validações que você queira realizar devem ser adicionadas à coleção `Validations` fornecida. Vamos ver como adicionar uma validação simples para exigir que o email do usuário seja válido. + +```swift +validations.add("email", as: String.self, is: .email) +``` + +O primeiro parâmetro é a chave esperada do valor, neste caso `"email"`. Isso deve corresponder ao nome da propriedade no tipo sendo validado. O segundo parâmetro, `as`, é o tipo esperado, neste caso `String`. O tipo geralmente corresponde ao tipo da propriedade, mas nem sempre. Por fim, um ou mais validadores podem ser adicionados após o terceiro parâmetro, `is`. Neste caso, estamos adicionando um único validador que verifica se o valor é um endereço de email. + +### Validando Conteúdo da Requisição + +Uma vez que você conformou seu tipo a `Validatable`, a função estática `validate(content:)` pode ser usada para validar o conteúdo da requisição. Adicione a seguinte linha antes de `req.content.decode(CreateUser.self)` no handler de rota. + +```swift +try CreateUser.validate(content: req) +``` + +Agora, tente enviar a seguinte requisição contendo um email inválido: + +```http +POST /users HTTP/1.1 +Content-Length: 67 +Content-Type: application/json + +{ + "age": 4, + "email": "foo", + "favoriteColor": "green", + "name": "Foo", + "username": "foo" +} +``` + +Você deverá ver o seguinte erro retornado: + +``` +email is not a valid email address +``` + +### Validando Query da Requisição + +Tipos em conformidade com `Validatable` também possuem `validate(query:)` que pode ser usado para validar a query string de uma requisição. Adicione as seguintes linhas ao handler de rota. + +```swift +try CreateUser.validate(query: req) +req.query.decode(CreateUser.self) +``` + +Agora, tente enviar a seguinte requisição contendo um email inválido na query string. + +```http +GET /users?age=4&email=foo&favoriteColor=green&name=Foo&username=foo HTTP/1.1 + +``` + +Você deverá ver o seguinte erro retornado: + +``` +email is not a valid email address +``` + +### Validação de Inteiros + +Ótimo, agora vamos tentar adicionar uma validação para `age`. + +```swift +validations.add("age", as: Int.self, is: .range(13...)) +``` + +A validação de idade exige que a idade seja maior ou igual a `13`. Se você tentar a mesma requisição de antes, deverá ver um novo erro agora: + +``` +age is less than minimum of 13, email is not a valid email address +``` + +### Validação de Strings + +Em seguida, vamos adicionar validações para `name` e `username`. + +```swift +validations.add("name", as: String.self, is: !.empty) +validations.add("username", as: String.self, is: .count(3...) && .alphanumeric) +``` + +A validação de nome usa o operador `!` para inverter a validação `.empty`. Isso exigirá que a string não esteja vazia. + +A validação de username combina dois validadores usando `&&`. Isso exigirá que a string tenha pelo menos 3 caracteres _e_ contenha apenas caracteres alfanuméricos. + +### Validação de Enum + +Por fim, vamos ver uma validação um pouco mais avançada para verificar se a `favoriteColor` fornecida é válida. + +```swift +validations.add( + "favoriteColor", as: String.self, + is: .in("red", "blue", "green"), + required: false +) +``` + +Como não é possível decodificar um `Color` a partir de um valor inválido, esta validação usa `String` como tipo base. Ela usa o validador `.in` para verificar se o valor é uma opção válida: red, blue ou green. Como este valor é opcional, `required` é definido como false para sinalizar que a validação não deve falhar se esta chave estiver ausente nos dados da requisição. + +Note que embora a validação de cor favorita passe se a chave estiver ausente, ela não passará se `null` for fornecido. Se você quiser suportar `null`, altere o tipo de validação para `String?` e use a conveniência `.nil ||` (leia como: "é nil ou ..."). + +```swift +validations.add( + "favoriteColor", as: String?.self, + is: .nil || .in("red", "blue", "green"), + required: false +) +``` + +### Erros Personalizados + +Você pode querer adicionar erros personalizados legíveis às suas `Validations` ou `Validator`. Para isso, simplesmente forneça o parâmetro adicional `customFailureDescription` que substituirá o erro padrão. + +```swift +validations.add( + "name", + as: String.self, + is: !.empty, + customFailureDescription: "Provided name is empty!" +) +validations.add( + "username", + as: String.self, + is: .count(3...) && .alphanumeric, + customFailureDescription: "Provided username is invalid!" +) +``` + + +## Validadores + +Abaixo está uma lista dos validadores atualmente suportados e uma breve explicação do que eles fazem. + +|Validação|Descrição| +|-|-| +|`.ascii`|Contém apenas caracteres ASCII.| +|`.alphanumeric`|Contém apenas caracteres alfanuméricos.| +|`.characterSet(_:)`|Contém apenas caracteres do `CharacterSet` fornecido.| +|`.count(_:)`|A contagem da coleção está dentro dos limites fornecidos.| +|`.email`|Contém um email válido.| +|`.empty`|A coleção está vazia.| +|`.in(_:)`|O valor está na `Collection` fornecida.| +|`.nil`|O valor é `null`.| +|`.range(_:)`|O valor está dentro do `Range` fornecido.| +|`.url`|Contém uma URL válida.| +|`.custom(_:, validationClosure: (value) -> Bool)`|Validação personalizada, de uso pontual.| + +Validadores também podem ser combinados para construir validações complexas usando operadores. Mais informações sobre o validador `.custom` em [[#Validadores Personalizados]]. + +|Operador|Posição|Descrição| +|-|-|-| +|`!`|prefixo|Inverte um validador, exigindo o oposto.| +|`&&`|infixo|Combina dois validadores, exige ambos.| +|`||`|infixo|Combina dois validadores, exige um.| + + + +## Validadores Personalizados + +Existem duas maneiras de criar validadores personalizados. + +### Estendendo a API de Validação + +Estender a API de Validação é mais adequado para casos onde você planeja usar o validador personalizado em mais de um objeto `Content`. Nesta seção, vamos guiá-lo pelos passos para criar um validador personalizado para validar códigos postais. + +Primeiro, crie um novo tipo para representar os resultados da validação de `ZipCode`. Esta struct será responsável por reportar se uma determinada string é um código postal válido. + +```swift +extension ValidatorResults { + /// Representa o resultado de um validador que verifica se uma string é um código postal válido. + public struct ZipCode { + /// Indica se a entrada é um código postal válido. + public let isValidZipCode: Bool + } +} +``` + +Em seguida, conforme o novo tipo a `ValidatorResult`, que define o comportamento esperado de um validador personalizado. + +```swift +extension ValidatorResults.ZipCode: ValidatorResult { + public var isFailure: Bool { + !self.isValidZipCode + } + + public var successDescription: String? { + "is a valid zip code" + } + + public var failureDescription: String? { + "is not a valid zip code" + } +} +``` + +Por fim, implemente a lógica de validação para códigos postais. Use uma expressão regular para verificar se a string de entrada corresponde ao formato de um código postal dos EUA. + +```swift +private let zipCodeRegex: String = "^\\d{5}(?:[-\\s]\\d{4})?$" + +extension Validator where T == String { + /// Valida se uma `String` é um código postal válido. + public static var zipCode: Validator { + .init { input in + guard let range = input.range(of: zipCodeRegex, options: [.regularExpression]), + range.lowerBound == input.startIndex && range.upperBound == input.endIndex + else { + return ValidatorResults.ZipCode(isValidZipCode: false) + } + return ValidatorResults.ZipCode(isValidZipCode: true) + } + } +} +``` + +Agora que você definiu o validador personalizado `zipCode`, pode usá-lo para validar códigos postais na sua aplicação. Simplesmente adicione a seguinte linha ao seu código de validação: + +```swift +validations.add("zipCode", as: String.self, is: .zipCode) +``` + +### Validador `Custom` + +O validador `Custom` é mais adequado para casos onde você quer validar uma propriedade em apenas um objeto `Content`. Esta implementação possui as seguintes duas vantagens em comparação com estender a API de Validação: + +- Mais simples de implementar lógica de validação personalizada. +- Sintaxe mais curta. + +Nesta seção, vamos guiá-lo pelos passos para criar um validador personalizado para verificar se um funcionário faz parte da nossa empresa, analisando a propriedade `nameAndSurname`. + +```swift +let allCompanyEmployees: [String] = [ + "Everett Erickson", + "Sabrina Manning", + "Seth Gates", + "Melina Hobbs", + "Brendan Wade", + "Evie Richardson", +] + +struct Employee: Content { + var nameAndSurname: String + var email: String + var age: Int + var role: String + + static func validations(_ validations: inout Validations) { + validations.add( + "nameAndSurname", + as: String.self, + is: .custom("Valida se o funcionário faz parte da empresa XYZ verificando nome e sobrenome.") { nameAndSurname in + for employee in allCompanyEmployees { + if employee == nameAndSurname { + return true + } + } + return false + } + ) + } +} +``` diff --git a/docs/contributing/contributing.pt.md b/docs/contributing/contributing.pt.md new file mode 100644 index 000000000..21adaacd5 --- /dev/null +++ b/docs/contributing/contributing.pt.md @@ -0,0 +1,57 @@ +# Contribuindo com o Vapor + +O Vapor é um projeto dirigido pela comunidade e contribuições de membros da comunidade formam uma parte significativa do desenvolvimento do Vapor. Este guia ajudará você a entender o processo de contribuição e a fazer seus primeiros commits no Vapor! + +Qualquer contribuição que você faça é útil! Até pequenas coisas como corrigir erros de digitação fazem uma grande diferença para as pessoas que usam o Vapor. + +## Code of Conduct + +O Vapor adotou o Code of Conduct do Swift, que pode ser encontrado em [https://www.swift.org/code-of-conduct/](https://www.swift.org/code-of-conduct/). Espera-se que todos os contribuidores sigam o code of conduct. + +## No que trabalhar + +Descobrir no que trabalhar pode ser um grande obstáculo quando se trata de começar em open source! Geralmente, as melhores coisas para trabalhar são issues que você encontra ou funcionalidades que você quer. No entanto, o Vapor tem algumas coisas úteis para ajudar você a contribuir. + +### Problemas de Segurança + +Se você descobrir um problema de segurança e quiser reportá-lo ou ajudar a corrigi-lo, por favor **não** abra uma issue ou crie um pull request. Temos um processo separado para problemas de segurança para garantir que não exponhamos vulnerabilidades até que uma correção esteja disponível. Envie um e-mail para security@vapor.codes ou [veja aqui](https://github.com/vapor/.github/blob/main/SECURITY.md) para mais detalhes. + +### Pequenas issues + +Se você encontrar uma pequena issue, bug ou erro de digitação, sinta-se à vontade para ir em frente e criar um pull request para corrigir. Se isso resolver uma issue aberta em algum dos repositórios, você pode vinculá-la no pull request na barra lateral para que a issue seja automaticamente fechada quando o pull request for mergeado. + +![GitHub Link Issue](../images/github-link-issue.png) + +### Novas funcionalidades + +Se você quiser propor mudanças maiores como novas funcionalidades ou correções de bugs que alteram quantidades significativas de código, por favor abra uma issue primeiro ou poste no canal `#development` no Discord. Isso nos permite discutir a mudança com você, pois pode haver algum contexto que precisamos aplicar ou podemos dar dicas. Não queremos que você perca tempo se uma funcionalidade não se encaixa nos nossos planos! + +### Boards do Vapor + +Se você quer contribuir mas não tem uma ideia do que trabalhar, isso é ótimo! O Vapor tem alguns boards que podem ajudar. O Vapor tem cerca de 40 repositórios que são ativamente desenvolvidos e procurar em todos eles para encontrar algo para trabalhar não é prático, então usamos boards para agregá-los. + +O primeiro board é o [good first issue board](https://github.com/orgs/vapor/projects/14). Qualquer issue na organização do Vapor no GitHub que esteja marcada com `good first issue` será adicionada ao board para você encontrar. São issues que achamos que serão boas para pessoas relativamente novas no Vapor trabalharem, pois não requerem muita experiência com o código. + +O segundo board é o [help wanted board](https://github.com/orgs/vapor/projects/13). Ele puxa issues marcadas com `help wanted`. São issues que podem ser boas de corrigir, mas o time core atualmente tem outras prioridades. Essas issues geralmente requerem um pouco mais de conhecimento se não estiverem também marcadas com `good first issue`, mas podem ser projetos divertidos para trabalhar! + +### Traduções + +A área final onde contribuições são extremamente valiosas é a documentação. Os docs têm traduções para múltiplos idiomas, mas nem toda página está traduzida e há muitos mais idiomas que gostaríamos de suportar! Se você tem interesse em contribuir com novos idiomas ou atualizações, veja o [README dos docs](https://github.com/vapor/docs#translating) ou entre em contato no canal `#documentation` no Discord. + +## Processo de Contribuição + +Se você nunca trabalhou em um projeto open source, os passos para realmente contribuir podem ser confusos, mas são bem simples. + +Primeiro, faça um fork do Vapor ou de qualquer repositório em que você queira trabalhar. Você pode fazer isso na interface do GitHub e o GitHub tem [documentação excelente](https://docs.github.com/en/get-started/quickstart/fork-a-repo) sobre como fazer isso. + +Você pode então fazer mudanças no seu fork com o processo usual de commit e push. Quando estiver pronto para submeter sua correção, pode criar um PR para o repositório do Vapor. Novamente, o GitHub tem [documentação excelente](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) sobre como fazer isso. + +## Submetendo um Pull Request + +Ao submeter um pull request, há várias coisas que você deve verificar: + +* Todos os testes passam +* Novos testes adicionados para qualquer novo comportamento ou bugs corrigidos +* Novas APIs públicas estão documentadas. Usamos DocC para nossa documentação de API. + +O Vapor usa automação para reduzir a quantidade de trabalho necessário para muitas tarefas. Para pull requests, usamos o [Vapor Bot](https://github.com/VaporBot) para gerar releases quando um pull request é mergeado. O corpo e título do pull request são usados para gerar as notas de release, então certifique-se de que façam sentido e cubram o que você esperaria ver em notas de release. Temos mais detalhes nas [diretrizes de contribuição do Vapor](https://github.com/vapor/vapor/blob/main/.github/contributing.md#release-title). diff --git a/docs/deploy/digital-ocean.pt.md b/docs/deploy/digital-ocean.pt.md new file mode 100644 index 000000000..3695ccbcf --- /dev/null +++ b/docs/deploy/digital-ocean.pt.md @@ -0,0 +1,184 @@ +# Deploy no DigitalOcean + +Este guia vai acompanhar você no processo de deploy de uma aplicação Vapor simples Hello, world em um [Droplet](https://www.digitalocean.com/products/droplets/). Para seguir este guia, você precisa ter uma conta no [DigitalOcean](https://www.digitalocean.com) com a cobrança configurada. + +## Criar Servidor + +Vamos começar instalando o Swift em um servidor Linux. Use o menu de criação para criar um novo Droplet. + +![Create Droplet](../images/digital-ocean-create-droplet.png) + +Em distribuições, selecione Ubuntu 22.04 LTS. O guia a seguir usará esta versão como exemplo. + +![Ubuntu Distro](../images/digital-ocean-distributions-ubuntu.png) + +!!! note "Nota" + Você pode selecionar qualquer distribuição Linux com uma versão que o Swift suporte. Você pode verificar quais sistemas operacionais são oficialmente suportados na página [Swift Releases](https://swift.org/download/#releases). + +Após selecionar a distribuição, escolha qualquer plano e região de datacenter que preferir. Em seguida, configure uma chave SSH para acessar o servidor após a criação. Por fim, clique em criar Droplet e aguarde o novo servidor ser provisionado. + +Quando o novo servidor estiver pronto, passe o mouse sobre o endereço IP do Droplet e clique em copiar. + +![Droplet List](../images/digital-ocean-droplet-list.png) + +## Configuração Inicial + +Abra seu terminal e conecte-se ao servidor como root usando SSH. + +```sh +ssh root@your_server_ip +``` + +O DigitalOcean tem um guia detalhado para [configuração inicial do servidor no Ubuntu 22.04](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-22-04). Este guia vai cobrir rapidamente o básico. + +### Configurar Firewall + +Permita o OpenSSH através do firewall e habilite-o. + +```sh +ufw allow OpenSSH +ufw enable +``` + +### Adicionar Usuário + +Crie um novo usuário além do `root`. Este guia chama o novo usuário de `vapor`. + +```sh +adduser vapor +``` + +Permita que o usuário recém-criado use o `sudo`. + +```sh +usermod -aG sudo vapor +``` + +Copie as chaves SSH autorizadas do usuário root para o usuário recém-criado. Isso permitirá que você se conecte via SSH como o novo usuário. + +```sh +rsync --archive --chown=vapor:vapor ~/.ssh /home/vapor +``` + +Por fim, saia da sessão SSH atual e faça login como o usuário recém-criado. + +```sh +exit +ssh vapor@your_server_ip +``` + +## Instalar Swift + +Agora que você criou um novo servidor Ubuntu e fez login como usuário não-root, você pode instalar o Swift. + +### Instalação automatizada usando a ferramenta CLI Swiftly (recomendado) + +Acesse o [site do Swiftly](https://swiftlang.github.io/swiftly/) para instruções sobre como instalar o Swiftly e o Swift no Linux. Depois disso, instale o Swift com o seguinte comando: + +#### Uso básico + +```sh +$ swiftly install latest + +Fetching the latest stable Swift release... +Installing Swift 5.9.1 +Downloaded 488.5 MiB of 488.5 MiB +Extracting toolchain... +Swift 5.9.1 installed successfully! + +$ swift --version + +Swift version 5.9.1 (swift-5.9.1-RELEASE) +Target: x86_64-unknown-linux-gnu +``` + +## Instalar Vapor Usando o Vapor Toolbox + +Agora que o Swift está instalado, vamos instalar o Vapor usando o Vapor Toolbox. Você precisará compilar o toolbox a partir do código-fonte. Veja os [releases](https://github.com/vapor/toolbox/releases) do toolbox no GitHub para encontrar a versão mais recente. Neste exemplo, estamos usando a 18.6.0. + +### Clonar e Compilar o Vapor + +Clone o repositório do Vapor Toolbox. + +```sh +git clone https://github.com/vapor/toolbox.git +``` + +Faça checkout da versão mais recente. + +```sh +cd toolbox +git checkout 18.6.0 +``` + +Compile o Vapor e mova o binário para o seu path. + +```sh +swift build -c release --disable-sandbox --enable-test-discovery +sudo mv .build/release/vapor /usr/local/bin +``` + +### Criar um Projeto Vapor + +Use o comando de novo projeto do Toolbox para iniciar um projeto. + +```sh +vapor new HelloWorld -n +``` + +!!! tip "Dica" + A flag `-n` fornece um template básico respondendo automaticamente não para todas as perguntas. + +![Vapor Splash](../images/vapor-splash.png) + +Quando o comando terminar, entre na pasta recém-criada: + +```sh +cd HelloWorld +``` + +### Abrir Porta HTTP + +Para acessar o Vapor no seu servidor, abra uma porta HTTP. + +```sh +sudo ufw allow 8080 +``` + +### Executar + +Agora que o Vapor está configurado e temos uma porta aberta, vamos executá-lo. + +```sh +swift run App serve --hostname 0.0.0.0 --port 8080 +``` + +Acesse o IP do seu servidor pelo navegador ou terminal local e você deverá ver "Funciona!". O endereço IP é `134.122.126.139` neste exemplo. + +``` +$ curl http://134.122.126.139:8080 +Funciona! +``` + +De volta ao seu servidor, você deverá ver logs da requisição de teste. + +``` +[ NOTICE ] Server starting on http://0.0.0.0:8080 +[ INFO ] GET / +``` + +Use `CTRL+C` para encerrar o servidor. Pode levar um segundo para desligar. + +Parabéns por ter sua aplicação Vapor rodando em um Droplet do DigitalOcean! + +## Próximos Passos + +O restante deste guia aponta para recursos adicionais para melhorar seu deploy. + +### Supervisor + +O Supervisor é um sistema de controle de processos que pode executar e monitorar seu executável Vapor. Com o Supervisor configurado, sua aplicação pode iniciar automaticamente quando o servidor inicializa e ser reiniciada em caso de falha. Saiba mais sobre o [Supervisor](../deploy/supervisor.md). + +### Nginx + +O Nginx é um servidor HTTP e proxy extremamente rápido, testado em batalha e fácil de configurar. Embora o Vapor suporte servir requisições HTTP diretamente, usar um proxy como o Nginx pode oferecer maior performance, segurança e facilidade de uso. Saiba mais sobre o [Nginx](../deploy/nginx.md). diff --git a/docs/deploy/docker.pt.md b/docs/deploy/docker.pt.md new file mode 100644 index 000000000..862d6278f --- /dev/null +++ b/docs/deploy/docker.pt.md @@ -0,0 +1,288 @@ +# Deploy com Docker + +Usar Docker para fazer o deploy da sua aplicação Vapor tem vários benefícios: + +1. Sua aplicação dockerizada pode ser iniciada de forma confiável usando os mesmos comandos em qualquer plataforma com um Docker Daemon — nomeadamente, Linux (CentOS, Debian, Fedora, Ubuntu), macOS e Windows. +2. Você pode usar docker-compose ou manifestos Kubernetes para orquestrar múltiplos serviços necessários para um deploy completo (ex: Redis, Postgres, nginx, etc.). +3. É fácil testar a capacidade da sua aplicação de escalar horizontalmente, mesmo localmente na sua máquina de desenvolvimento. + +Este guia não vai explicar como levar sua aplicação dockerizada para um servidor. O deploy mais simples envolveria instalar o Docker no seu servidor e executar os mesmos comandos que você executaria na sua máquina de desenvolvimento para iniciar sua aplicação. + +Deploys mais complicados e robustos geralmente diferem dependendo da sua solução de hospedagem; muitas soluções populares como AWS têm suporte integrado para Kubernetes e soluções de banco de dados customizadas, o que torna difícil escrever melhores práticas de uma forma que se aplique a todos os deploys. + +No entanto, usar Docker para iniciar toda a sua stack de servidor localmente para fins de teste é incrivelmente valioso tanto para aplicações serverside grandes quanto pequenas. Além disso, os conceitos descritos neste guia se aplicam em linhas gerais a todos os deploys com Docker. + +## Configuração + +Você precisará configurar seu ambiente de desenvolvimento para executar o Docker e obter um entendimento básico dos arquivos de recursos que configuram stacks Docker. + +### Instalar Docker + +Você precisará instalar o Docker para seu ambiente de desenvolvimento. Você pode encontrar informações para qualquer plataforma na seção [Supported Platforms](https://docs.docker.com/install/#supported-platforms) da visão geral do Docker Engine. Se você está no macOS, pode ir diretamente para a página de instalação do [Docker for Mac](https://docs.docker.com/docker-for-mac/install/). + +### Gerar Template + +Sugerimos usar o template do Vapor como ponto de partida. Se você já tem uma aplicação, compile o template conforme descrito abaixo em uma nova pasta como ponto de referência enquanto dockeriza sua aplicação existente — você pode copiar recursos-chave do template para sua aplicação e ajustá-los levemente como ponto de partida. + +1. Instale ou compile o Vapor Toolbox ([macOS](../install/macos.md#install-toolbox), [Linux](../install/linux.md#install-toolbox)). +2. Crie uma nova aplicação Vapor com `vapor new my-dockerized-app` e siga os prompts para habilitar ou desabilitar funcionalidades relevantes. Suas respostas a esses prompts afetarão como os arquivos de recursos Docker são gerados. + +## Recursos Docker + +Vale a pena, seja agora ou em breve, familiarizar-se com a [Visão Geral do Docker](https://docs.docker.com/engine/docker-overview/). A visão geral vai explicar algumas terminologias-chave que este guia utiliza. + +O template de aplicação Vapor tem dois recursos-chave específicos do Docker: um **Dockerfile** e um arquivo **docker-compose**. + +### Dockerfile + +Um Dockerfile diz ao Docker como compilar uma imagem da sua aplicação dockerizada. Essa imagem contém tanto o executável da sua aplicação quanto todas as dependências necessárias para executá-la. A [referência completa](https://docs.docker.com/engine/reference/builder/) vale a pena manter aberta quando estiver trabalhando na customização do seu Dockerfile. + +O Dockerfile gerado para sua aplicação Vapor tem dois estágios. O primeiro estágio compila sua aplicação e configura uma área de espera contendo o resultado. O segundo estágio configura o básico de um ambiente de execução seguro, transfere tudo na área de espera para onde ficará na imagem final, e define um entrypoint e comando padrão que executará sua aplicação em modo de produção na porta padrão (8080). Esta configuração pode ser sobrescrita quando a imagem é utilizada. + +### Arquivo Docker Compose + +Um arquivo Docker Compose define a forma como o Docker deve compilar múltiplos serviços em relação uns aos outros. O arquivo Docker Compose no template de aplicação Vapor fornece a funcionalidade necessária para fazer o deploy da sua aplicação, mas se você quiser saber mais, consulte a [referência completa](https://docs.docker.com/compose/compose-file/) que tem detalhes sobre todas as opções disponíveis. + +!!! note "Nota" + Se você planeja usar Kubernetes para orquestrar sua aplicação, o arquivo Docker Compose não é diretamente relevante. No entanto, os arquivos de manifesto do Kubernetes são conceitualmente similares e existem até projetos voltados para [converter arquivos Docker Compose](https://kubernetes.io/docs/tasks/configure-pod-container/translate-compose-kubernetes/) em manifestos Kubernetes. + +O arquivo Docker Compose na sua nova aplicação Vapor definirá serviços para executar sua aplicação, executar migrações ou revertê-las, e executar um banco de dados como camada de persistência da sua aplicação. As definições exatas variam dependendo de qual banco de dados você escolheu ao executar `vapor new`. + +Note que seu arquivo Docker Compose tem algumas variáveis de ambiente compartilhadas próximo ao topo. (Você pode ter um conjunto diferente de variáveis padrão dependendo de estar ou não usando o Fluent e qual driver do Fluent está em uso.) + +```docker +x-shared_environment: &shared_environment + LOG_LEVEL: ${LOG_LEVEL:-debug} + DATABASE_HOST: db + DATABASE_NAME: vapor_database + DATABASE_USERNAME: vapor_username + DATABASE_PASSWORD: vapor_password +``` + +Você verá essas variáveis sendo puxadas para múltiplos serviços abaixo com a sintaxe de referência YAML `<<: *shared_environment`. + +As variáveis `DATABASE_HOST`, `DATABASE_NAME`, `DATABASE_USERNAME` e `DATABASE_PASSWORD` estão fixas no código neste exemplo, enquanto o `LOG_LEVEL` receberá seu valor do ambiente executando o serviço ou fará fallback para `'debug'` se a variável não estiver definida. + +!!! note "Nota" + Fixar o nome de usuário e senha no código é aceitável para desenvolvimento local, mas você deve armazenar essas variáveis em um arquivo de secrets para deploy em produção. Uma forma de lidar com isso em produção é exportar o arquivo de secrets para o ambiente que está executando seu deploy e usar linhas como a seguinte no seu arquivo Docker Compose: + + ``` + DATABASE_USERNAME: ${DATABASE_USERNAME} + ``` + + Isso passa a variável de ambiente para os containers como definida pelo host. + +Outras coisas a observar: + +- Dependências de serviço são definidas por arrays `depends_on`. +- Portas de serviço são expostas ao sistema executando os serviços com arrays `ports` (formatadas como `:`). +- O `DATABASE_HOST` é definido como `db`. Isso significa que sua aplicação acessará o banco de dados em `http://db:5432`. Isso funciona porque o Docker criará uma rede em uso pelos seus serviços e o DNS interno nessa rede roteará o nome `db` para o serviço chamado `'db'`. +- A diretiva `CMD` no Dockerfile é sobrescrita em alguns serviços com o array `command`. Note que o que é especificado por `command` é executado contra o `ENTRYPOINT` no Dockerfile. +- No Swarm Mode (mais sobre isso abaixo) os serviços por padrão receberão 1 instância, mas os serviços `migrate` e `revert` são definidos como tendo `deploy` `replicas: 0` para que não iniciem por padrão ao executar um Swarm. + +## Compilando + +O arquivo Docker Compose diz ao Docker como compilar sua aplicação (usando o Dockerfile no diretório atual) e como nomear a imagem resultante (`my-dockerized-app:latest`). Este último é na verdade a combinação de um nome (`my-dockerized-app`) e uma tag (`latest`) onde tags são usadas para versionar imagens Docker. + +Para compilar uma imagem Docker da sua aplicação, execute + +```shell +docker compose build +``` + +no diretório raiz do projeto da sua aplicação (a pasta contendo `docker-compose.yml`). + +Você verá que sua aplicação e suas dependências precisam ser compiladas novamente mesmo se você as tiver compilado anteriormente na sua máquina de desenvolvimento. Elas estão sendo compiladas no ambiente de compilação Linux que o Docker está usando, então os artefatos de compilação da sua máquina de desenvolvimento não são reutilizáveis. + +Quando terminar, você encontrará a imagem da sua aplicação ao executar + +```shell +docker image ls +``` + +## Executando + +Sua stack de serviços pode ser executada diretamente a partir do arquivo Docker Compose ou você pode usar uma camada de orquestração como Swarm Mode ou Kubernetes. + +### Standalone + +A forma mais simples de executar sua aplicação é iniciá-la como um container standalone. O Docker usará os arrays `depends_on` para garantir que quaisquer serviços dependentes também sejam iniciados. + +Primeiro, execute: + +```shell +docker compose up app +``` + +e note que tanto o serviço `app` quanto o `db` são iniciados. + +Sua aplicação está escutando na porta 8080 e, conforme definido pelo arquivo Docker Compose, ela é acessível na sua máquina de desenvolvimento em **http://localhost:8080**. + +Essa distinção de mapeamento de portas é muito importante porque você pode executar qualquer número de serviços nas mesmas portas se todos estiverem rodando em seus próprios containers e cada um expor portas diferentes para a máquina host. + +Acesse `http://localhost:8080` e você verá `Funciona!`, mas acesse `http://localhost:8080/todos` e você receberá: + +``` +{"error":true,"reason":"Something went wrong."} +``` + +Dê uma olhada nos logs no terminal onde você executou `docker compose up app` e verá: + +``` +[ ERROR ] relation "todos" does not exist +``` + +Claro! Precisamos executar as migrações no banco de dados. Pressione `Ctrl+C` para encerrar sua aplicação. Vamos iniciar a aplicação novamente, mas desta vez com: + +```shell +docker compose up --detach app +``` + +Agora sua aplicação vai iniciar "desanexada" (em segundo plano). Você pode verificar isso executando: + +```shell +docker container ls +``` + +onde verá tanto o banco de dados quanto sua aplicação rodando em containers. Você pode até verificar os logs executando: + +```shell +docker logs +``` + +Para executar as migrações, execute: + +```shell +docker compose run migrate +``` + +Após as migrações, você pode acessar `http://localhost:8080/todos` novamente e receberá uma lista vazia de todos em vez de uma mensagem de erro. + +#### Níveis de Log + +Lembre-se que a variável de ambiente `LOG_LEVEL` no arquivo Docker Compose será herdada do ambiente onde o serviço é iniciado, se disponível. + +Você pode iniciar seus serviços com + +```shell +LOG_LEVEL=trace docker-compose up app +``` + +para obter logging no nível `trace` (o mais granular). Você pode usar essa variável de ambiente para definir o logging em [qualquer nível disponível](../basics/logging.md#levels). + +#### Logs de Todos os Serviços + +Se você especificar explicitamente seu serviço de banco de dados ao iniciar os containers, verá logs tanto do seu banco de dados quanto da sua aplicação. + +```shell +docker-compose up app db +``` + +#### Encerrando Containers Standalone + +Agora que você tem containers rodando "desanexados" do seu shell host, precisa dizer a eles para desligarem de alguma forma. Vale saber que qualquer container em execução pode ser solicitado a encerrar com + +```shell +docker container stop +``` + +mas a forma mais fácil de encerrar esses containers específicos é + +```shell +docker-compose down +``` + +#### Limpando o Banco de Dados + +O arquivo Docker Compose define um volume `db_data` para persistir seu banco de dados entre execuções. Existem algumas formas de resetar seu banco de dados. + +Você pode remover o volume `db_data` ao mesmo tempo que encerra seus containers com + +```shell +docker-compose down --volumes +``` + +Você pode ver quaisquer volumes atualmente persistindo dados com `docker volume ls`. Note que o nome do volume geralmente terá um prefixo de `my-dockerized-app_` ou `test_` dependendo de estar executando no Swarm Mode ou não. + +Você pode remover esses volumes um de cada vez com, por exemplo: + +```shell +docker volume rm my-dockerized-app_db_data +``` + +Você também pode limpar todos os volumes com + +```shell +docker volume prune +``` + +Apenas tenha cuidado para não remover acidentalmente um volume com dados que você queria manter! + +O Docker não permitirá que você remova volumes que estão atualmente em uso por containers em execução ou parados. Você pode obter uma lista de containers em execução com `docker container ls` e pode ver containers parados também com `docker container ls -a`. + +### Swarm Mode + +O Swarm Mode é uma interface fácil de usar quando você tem um arquivo Docker Compose em mãos e quer testar como sua aplicação escala horizontalmente. Você pode ler tudo sobre o Swarm Mode nas páginas a partir da [visão geral](https://docs.docker.com/engine/swarm/). + +A primeira coisa que precisamos é de um nó manager para nosso Swarm. Execute + +```shell +docker swarm init +``` + +Em seguida, usaremos nosso arquivo Docker Compose para iniciar uma stack chamada `'test'` contendo nossos serviços + +```shell +docker stack deploy -c docker-compose.yml test +``` + +Podemos ver como nossos serviços estão com + +```shell +docker service ls +``` + +Você deve esperar ver `1/1` réplicas para seus serviços `app` e `db` e `0/0` réplicas para seus serviços `migrate` e `revert`. + +Precisamos usar um comando diferente para executar migrações no Swarm mode. + +```shell +docker service scale --detach test_migrate=1 +``` + +!!! note "Nota" + Acabamos de solicitar que um serviço de curta duração escale para 1 réplica. Ele vai escalar com sucesso, executar e depois encerrar. No entanto, isso o deixará com `0/1` réplicas em execução. Isso não é um grande problema até querermos executar migrações novamente, mas não podemos dizer para "escalar para 1 réplica" se já é onde ele está. Uma peculiaridade dessa configuração é que da próxima vez que quisermos executar migrações dentro do mesmo runtime do Swarm, precisamos primeiro escalar o serviço para `0` e depois de volta para `1`. + +A recompensa pelo nosso trabalho no contexto deste breve guia é que agora podemos escalar nossa aplicação para o que quisermos para testar quão bem ela lida com contenção de banco de dados, falhas e mais. + +Se você quiser executar 5 instâncias da sua aplicação concorrentemente, execute + +```shell +docker service scale test_app=5 +``` + +Além de observar o Docker escalar sua aplicação, você pode ver que 5 réplicas estão de fato em execução verificando novamente `docker service ls`. + +Você pode visualizar (e acompanhar) os logs da sua aplicação com + +```shell +docker service logs -f test_app +``` + +#### Encerrando Serviços do Swarm + +Quando quiser encerrar seus serviços no Swarm Mode, faça isso removendo a stack que você criou anteriormente. + +```shell +docker stack rm test +``` + +## Deploys em Produção + +Como mencionado no início, este guia não vai entrar em grandes detalhes sobre o deploy da sua aplicação dockerizada em produção porque o tópico é extenso e varia muito dependendo do serviço de hospedagem (AWS, Azure, etc.), ferramentas (Terraform, Ansible, etc.) e orquestração (Docker Swarm, Kubernetes, etc.). + +No entanto, as técnicas que você aprende para executar sua aplicação dockerizada localmente na sua máquina de desenvolvimento são amplamente transferíveis para ambientes de produção. Uma instância de servidor configurada para executar o daemon do Docker aceitará todos os mesmos comandos. + +Copie os arquivos do seu projeto para o servidor, conecte-se via SSH no servidor e execute um comando `docker-compose` ou `docker stack deploy` para colocar as coisas funcionando remotamente. + +Alternativamente, defina sua variável de ambiente `DOCKER_HOST` local para apontar para o seu servidor e execute os comandos `docker` localmente na sua máquina. É importante notar que com essa abordagem, você não precisa copiar nenhum dos arquivos do seu projeto para o servidor, _mas_ você precisa hospedar sua imagem Docker em algum lugar de onde seu servidor possa baixá-la. diff --git a/docs/deploy/fly.pt.md b/docs/deploy/fly.pt.md new file mode 100644 index 000000000..6859449f9 --- /dev/null +++ b/docs/deploy/fly.pt.md @@ -0,0 +1,190 @@ +# Fly + +Fly é uma plataforma de hospedagem que permite executar aplicações de servidor e bancos de dados com foco em edge computing. Veja [o site deles](https://fly.io/) para mais informações. + +!!! note "Nota" + Os comandos especificados neste documento estão sujeitos à [precificação do Fly](https://fly.io/docs/about/pricing/), certifique-se de entendê-la corretamente antes de continuar. + +## Criando uma Conta +Se você não tem uma conta, precisará [criar uma](https://fly.io/app/sign-up). + +## Instalando o flyctl +A principal forma de interagir com o Fly é usando a ferramenta CLI dedicada, `flyctl`, que você precisará instalar. + +### macOS +```bash +brew install flyctl +``` + +### Linux +```bash +curl -L https://fly.io/install.sh | sh +``` + +### Outras opções de instalação +Para mais opções e detalhes, veja a [documentação de instalação do `flyctl`](https://fly.io/docs/flyctl/install/). + +## Fazendo Login +Para fazer login pelo seu terminal, execute o seguinte comando: +```bash +fly auth login +``` + +## Configurando seu Projeto Vapor +Antes de fazer o deploy no Fly, você deve garantir que tem um projeto Vapor com um Dockerfile adequadamente configurado, já que ele é necessário para o Fly compilar sua aplicação. Na maioria dos casos, isso deve ser muito fácil, pois os templates padrão do Vapor já contêm um. + +### Novo Projeto Vapor +A forma mais fácil de criar um novo projeto é começar com um template. Você pode criar um usando os templates do GitHub ou o Vapor toolbox. Se você precisar de um banco de dados, é recomendado usar o Fluent com Postgres; o Fly facilita a criação de um banco de dados Postgres para conectar suas aplicações (veja a [seção dedicada](#configurando-postgres) abaixo). + +#### Usando o Vapor toolbox +Primeiro, certifique-se de ter instalado o Vapor toolbox (veja as instruções de instalação para [macOS](../install/macos.md#install-toolbox) ou [Linux](../install/linux.md#install-toolbox)). +Crie sua nova aplicação com o seguinte comando, substituindo `app-name` pelo nome desejado: +```bash +vapor new app-name +``` + +Este comando exibirá um prompt interativo que permitirá configurar seu projeto Vapor, onde você pode selecionar Fluent e Postgres se precisar deles. + +#### Usando templates do GitHub +Escolha o template que melhor atenda às suas necessidades na lista a seguir. Você pode cloná-lo localmente usando Git ou criar um projeto no GitHub com o botão "Use this template". + +- [Template básico](https://github.com/vapor/template-bare) +- [Template Fluent/Postgres](https://github.com/vapor/template-fluent-postgres) +- [Template Fluent/Postgres + Leaf](https://github.com/vapor/template-fluent-postgres-leaf) + +### Projeto Vapor Existente +Se você tem um projeto Vapor existente, certifique-se de ter um `Dockerfile` corretamente configurado na raiz do seu diretório; a [documentação do Vapor sobre uso do Docker](../deploy/docker.md) e a [documentação do Fly sobre deploy de uma aplicação via Dockerfile](https://fly.io/docs/languages-and-frameworks/dockerfile/) podem ser úteis. + +## Lançar sua Aplicação no Fly +Quando seu projeto Vapor estiver pronto, você pode lançá-lo no Fly. + +Primeiro, certifique-se de que seu diretório atual está na raiz da sua aplicação Vapor e execute o seguinte comando: +```bash +fly launch +``` + +Isso iniciará um prompt interativo para configurar as definições da sua aplicação no Fly: + +- **Nome:** você pode digitar um ou deixar em branco para obter um nome gerado automaticamente. +- **Região:** o padrão é a mais próxima de você. Você pode escolher usá-la ou qualquer outra da lista. Isso é fácil de alterar depois. +- **Banco de dados:** você pode pedir ao Fly para criar um banco de dados para usar com sua aplicação. Se preferir, você pode fazer o mesmo depois com os comandos `fly pg create` e `fly pg attach` (veja a [seção Configurando Postgres](#configurando-postgres) para mais detalhes). + +O comando `fly launch` cria automaticamente um arquivo `fly.toml`. Ele contém configurações como mapeamentos de portas públicas/privadas, parâmetros de health checks, entre outros. Se você acabou de criar um novo projeto do zero usando `vapor new`, o arquivo `fly.toml` padrão não precisa de alterações. Se você tem um projeto existente, é provável que o `fly.toml` também esteja ok sem alterações ou com apenas pequenas mudanças. Você pode encontrar mais informações na [documentação do `fly.toml`](https://fly.io/docs/reference/configuration/). + +Note que se você solicitar ao Fly para criar um banco de dados, precisará esperar um pouco para que ele seja criado e passe nos health checks. + +Antes de encerrar, o comando `fly launch` perguntará se você gostaria de fazer o deploy da sua aplicação imediatamente. Você pode aceitar ou fazer depois usando `fly deploy`. + +!!! tip "Dica" + Quando seu diretório atual está na raiz da sua aplicação, a ferramenta CLI do Fly detecta automaticamente a presença de um arquivo `fly.toml`, o que permite ao Fly saber qual aplicação seus comandos estão direcionando. Se você quiser direcionar uma aplicação específica independentemente do seu diretório atual, pode adicionar `-a nome-da-sua-app` à maioria dos comandos do Fly. + +## Deploy +Execute o comando `fly deploy` sempre que precisar fazer o deploy de novas alterações no Fly. + +O Fly lê os arquivos `Dockerfile` e `fly.toml` do seu diretório para determinar como compilar e executar seu projeto Vapor. + +Uma vez que seu container é compilado, o Fly inicia uma instância dele. Ele executará vários health checks, garantindo que sua aplicação está funcionando corretamente e que seu servidor responde a requisições. O comando `fly deploy` encerra com erro se os health checks falharem. + +Por padrão, o Fly fará rollback para a última versão funcional da sua aplicação se os health checks falharem para a nova versão que você tentou fazer deploy. + +Ao fazer o deploy de um worker em background (com Vapor Queues), não altere o CMD ou ENTRYPOINT no seu Dockerfile; deixe como está para que a aplicação web principal inicie normalmente. Em vez disso, adicione uma seção [processes] no seu arquivo fly.toml assim: + +``` +[processes] + app = "" + worker = "queues" +``` + +Isso diz ao Fly.io para executar o processo app com o entrypoint padrão do Docker (seu servidor web) e o processo worker para executar sua fila de jobs usando a interface de linha de comando do Vapor (ou seja, swift run App queues). + +## Configurando Postgres + +### Criando um banco de dados Postgres no Fly +Se você não criou uma aplicação de banco de dados quando lançou sua aplicação pela primeira vez, pode fazer isso depois usando: +```bash +fly pg create +``` + +Este comando cria uma aplicação Fly que será capaz de hospedar bancos de dados disponíveis para suas outras aplicações no Fly, veja a [documentação dedicada do Fly](https://fly.io/docs/postgres/) para mais detalhes. + +Uma vez que sua aplicação de banco de dados esteja criada, vá para o diretório raiz da sua aplicação Vapor e execute: +```bash +fly pg attach name-of-your-postgres-app +``` +Se você não sabe o nome da sua aplicação Postgres, pode encontrá-lo com `fly pg list`. + +O comando `fly pg attach` cria um banco de dados e usuário destinados à sua aplicação, e então os expõe à sua aplicação através da variável de ambiente `DATABASE_URL`. + +!!! note "Nota" + A diferença entre `fly pg create` e `fly pg attach` é que o primeiro aloca e configura uma aplicação Fly que será capaz de hospedar bancos de dados Postgres, enquanto o segundo cria um banco de dados e usuário reais destinados à aplicação de sua escolha. Desde que atenda aos seus requisitos, uma única aplicação Postgres no Fly pode hospedar múltiplos bancos de dados usados por várias aplicações. Quando você pede ao Fly para criar uma aplicação de banco de dados no `fly launch`, ele faz o equivalente a chamar tanto `fly pg create` quanto `fly pg attach`. + +### Conectando sua aplicação Vapor ao banco de dados +Uma vez que sua aplicação está conectada ao banco de dados, o Fly define a variável de ambiente `DATABASE_URL` com a URL de conexão que contém suas credenciais (ela deve ser tratada como informação sensível). + +Com a maioria das configurações comuns de projetos Vapor, você configura seu banco de dados em `configure.swift`. Veja como você pode fazer isso: + +```swift +if let databaseURL = Environment.get("DATABASE_URL") { + try app.databases.use(.postgres(url: databaseURL), as: .psql) +} else { + // Trate a DATABASE_URL ausente aqui... + // + // Alternativamente, você também pode definir uma configuração diferente + // dependendo se app.environment está definido como + // `.development` ou `.production` +} +``` + +Neste ponto, seu projeto deve estar pronto para executar migrações e usar o banco de dados. + +### Executando migrações +Com o `release_command` do `fly.toml`, você pode pedir ao Fly para executar um determinado comando antes de iniciar seu processo principal do servidor. Adicione isso ao `fly.toml`: +```toml +[deploy] + release_command = "migrate -y" +``` + +!!! note "Nota" + O trecho de código acima assume que você está usando o Dockerfile padrão do Vapor que define o `ENTRYPOINT` da sua aplicação como `./App`. Concretamente, isso significa que quando você define `release_command` como `migrate -y`, o Fly chamará `./App migrate -y`. Se seu `ENTRYPOINT` estiver definido com um valor diferente, você precisará adaptar o valor de `release_command`. + +O Fly executará seu release command em uma instância temporária que tem acesso à sua rede interna do Fly, secrets e variáveis de ambiente. + +Se seu release command falhar, o deploy não continuará. + +### Outros bancos de dados +Embora o Fly facilite a criação de uma aplicação de banco de dados Postgres, é possível hospedar outros tipos de bancos de dados também (por exemplo, veja ["Use a MySQL database"](https://fly.io/docs/app-guides/mysql-on-fly/) na documentação do Fly). + +## Secrets e variáveis de ambiente +### Secrets +Use secrets para definir quaisquer valores sensíveis como variáveis de ambiente. +```bash + fly secrets set MYSECRET=A_SUPER_SECRET_VALUE +``` + +!!! warning "Aviso" + Tenha em mente que a maioria dos shells mantém um histórico dos comandos que você digitou. Tenha cuidado com isso ao definir secrets dessa forma. Alguns shells podem ser configurados para não lembrar comandos que são precedidos por um espaço. Veja também o [comando `fly secrets import`](https://fly.io/docs/flyctl/secrets-import/). + +Para mais informações, veja a [documentação do `fly secrets`](https://fly.io/docs/apps/secrets/). + +### Variáveis de ambiente +Você pode definir outras variáveis de ambiente não sensíveis no [`fly.toml`](https://fly.io/docs/reference/configuration/#the-env-variables-section), por exemplo: +```toml +[env] + MAX_API_RETRY_COUNT = "3" + SMS_LOG_LEVEL = "error" +``` + +## Conexão SSH +Você pode conectar-se às instâncias de uma aplicação usando: +```bash +fly ssh console -s +``` + +## Verificando os logs +Você pode verificar os logs em tempo real da sua aplicação usando: +```bash +fly logs +``` + +## Próximos passos +Agora que sua aplicação Vapor está implantada, há muito mais que você pode fazer, como escalar suas aplicações vertical e horizontalmente em múltiplas regiões, adicionar volumes persistentes, configurar deploy contínuo, ou até criar clusters de aplicações distribuídas. O melhor lugar para aprender como fazer tudo isso e mais é a [documentação do Fly](https://fly.io/docs/). diff --git a/docs/deploy/heroku.pt.md b/docs/deploy/heroku.pt.md new file mode 100644 index 000000000..be699d446 --- /dev/null +++ b/docs/deploy/heroku.pt.md @@ -0,0 +1,236 @@ +# O que é o Heroku + +Heroku é uma solução de hospedagem completa e popular, você pode encontrar mais em [heroku.com](https://www.heroku.com) + +## Criando uma Conta + +Você precisará de uma conta no Heroku, se não tiver uma, por favor cadastre-se aqui: [https://signup.heroku.com/](https://signup.heroku.com/) + +## Instalando o CLI + +Certifique-se de ter instalado a ferramenta CLI do Heroku. + +### HomeBrew + +```bash +brew tap heroku/brew && brew install heroku +``` + +### Outras Opções de Instalação + +Veja opções alternativas de instalação aqui: [https://devcenter.heroku.com/articles/heroku-cli#download-and-install](https://devcenter.heroku.com/articles/heroku-cli#download-and-install). + +### Fazendo Login + +Depois de instalar o CLI, faça login com o seguinte comando: + +```bash +heroku login +``` + +Verifique se o e-mail correto está logado com: + +```bash +heroku auth:whoami +``` + +### Criar uma Aplicação + +Acesse dashboard.heroku.com para entrar na sua conta e crie uma nova aplicação no menu suspenso no canto superior direito. O Heroku fará algumas perguntas como região e nome da aplicação, basta seguir as instruções. + +### Git + +O Heroku usa Git para fazer o deploy da sua aplicação, então você precisará colocar seu projeto em um repositório Git, se ainda não estiver. + +#### Inicializar Git + +Se você precisa adicionar o Git ao seu projeto, digite o seguinte comando no Terminal: + +```bash +git init +``` + +#### Main + +Você deve escolher uma branch e manter essa para fazer deploy no Heroku, como a branch **main** ou **master**. Certifique-se de que todas as alterações estejam comitadas nesta branch antes de fazer push. + +Verifique sua branch atual com: + +```bash +git branch +``` + +O asterisco indica a branch atual. + +```bash +* main + commander + other-branches +``` + +!!! note "Nota" + Se você não vê nenhuma saída e acabou de executar `git init`, você precisará comitar seu código primeiro, então verá a saída do comando `git branch`. + +Se você _não_ está atualmente na branch correta, mude para ela digitando (para **main**): + +```bash +git checkout main +``` + +#### Comitar alterações + +Se este comando produzir saída, então você tem alterações não comitadas. + +```bash +git status --porcelain +``` + +Comite-as com o seguinte: + +```bash +git add . +git commit -m "a description of the changes I made" +``` + +#### Conectar com o Heroku + +Conecte sua aplicação com o Heroku (substitua pelo nome da sua aplicação). + +```bash +$ heroku git:remote -a your-apps-name-here +``` + +### Configurar Buildpack + +Configure o buildpack para ensinar o Heroku como lidar com o Vapor. + +```bash +heroku buildpacks:set vapor/vapor +``` + +### Arquivo de versão do Swift + +O buildpack que adicionamos procura um arquivo **.swift-version** para saber qual versão do Swift usar. (Substitua 5.8.1 pela versão que seu projeto requer.) + +```bash +echo "5.8.1" > .swift-version +``` + +Isso cria o **.swift-version** com `5.8.1` como seu conteúdo. + +### Procfile + +O Heroku usa o **Procfile** para saber como executar sua aplicação, no nosso caso ele precisa se parecer com isso: + +``` +web: App serve --env production --hostname 0.0.0.0 --port $PORT +``` + +Podemos criar isso com o seguinte comando no terminal: + +```bash +echo "web: App serve --env production" \ + "--hostname 0.0.0.0 --port \$PORT" > Procfile +``` + +### Comitar alterações + +Acabamos de adicionar esses arquivos, mas eles não estão comitados. Se fizermos push, o Heroku não os encontrará. + +Comite-os com o seguinte: + +```bash +git add . +git commit -m "adding heroku build files" +``` + +### Deploy no Heroku + +Você está pronto para o deploy, execute isso no terminal. Pode demorar um pouco para compilar, isso é normal. + +```bash +git push heroku main +``` + +### Escalar + +Depois de compilar com sucesso, você precisa adicionar pelo menos um servidor. Os preços começam em $5/mês para o plano Eco (veja [preços](https://www.heroku.com/pricing#containers)), certifique-se de ter o pagamento configurado no Heroku. Então, para um único web worker: + +```bash +heroku ps:scale web=1 +``` + +### Deploy Contínuo + +Sempre que quiser atualizar, basta trazer as últimas alterações para a main e fazer push para o Heroku, e ele fará o redeploy. + +## Postgres + +### Adicionar banco de dados PostgreSQL + +Acesse sua aplicação em dashboard.heroku.com e vá para a seção **Add-ons**. + +A partir daqui, digite `postgres` e você verá uma opção para `Heroku Postgres`. Selecione-a. + +Escolha o plano Essential 0 por $5/mês (veja [preços](https://www.heroku.com/pricing#data-services)) e provisione. O Heroku fará o resto. + +Quando terminar, você verá o banco de dados aparecer na aba **Resources**. + +### Configurar o banco de dados + +Agora precisamos informar à nossa aplicação como acessar o banco de dados. No diretório da nossa aplicação, vamos executar: + +```bash +heroku config +``` + +Isso produzirá uma saída parecida com esta: + +```none +=== today-i-learned-vapor Config Vars +DATABASE_URL: postgres://cybntsgadydqzm:2d9dc7f6d964f4750da1518ad71hag2ba729cd4527d4a18c70e024b11cfa8f4b@ec2-54-221-192-231.compute-1.amazonaws.com:5432/dfr89mvoo550b4 +``` + +**DATABASE_URL** aqui representará nosso banco de dados Postgres. **NUNCA** coloque a URL estática diretamente no código, o Heroku vai rotacioná-la e isso quebrará sua aplicação. Também é uma má prática. Em vez disso, leia a variável de ambiente em tempo de execução. + +O addon Heroku Postgres [requer](https://devcenter.heroku.com/changelog-items/2035) que todas as conexões sejam criptografadas. Os certificados usados pelos servidores Postgres são internos ao Heroku, portanto uma conexão TLS **não verificada** deve ser configurada. + +O trecho a seguir mostra como fazer ambos: + +```swift +if let databaseURL = Environment.get("DATABASE_URL") { + var tlsConfig: TLSConfiguration = .makeClientConfiguration() + tlsConfig.certificateVerification = .none + let nioSSLContext = try NIOSSLContext(configuration: tlsConfig) + + var postgresConfig = try SQLPostgresConfiguration(url: databaseURL) + postgresConfig.coreConfiguration.tls = .require(nioSSLContext) + + app.databases.use(.postgres(configuration: postgresConfig), as: .psql) +} else { + // ... +} +``` + +Não esqueça de comitar essas alterações + +```bash +git add . +git commit -m "configured heroku database" +``` + +### Revertendo seu banco de dados + +Você pode reverter ou executar outros comandos no Heroku com o comando `run`. + +Para reverter seu banco de dados: + +```bash +heroku run App -- migrate --revert --all --yes --env production +``` + +Para migrar: + +```bash +heroku run App -- migrate --env production +``` diff --git a/docs/deploy/nginx.pt.md b/docs/deploy/nginx.pt.md new file mode 100644 index 000000000..f8c8e9890 --- /dev/null +++ b/docs/deploy/nginx.pt.md @@ -0,0 +1,165 @@ +# Deploy com Nginx + +Nginx é um servidor HTTP e proxy extremamente rápido, testado em batalha e fácil de configurar. Embora o Vapor suporte servir requisições HTTP diretamente com ou sem TLS, usar um proxy como o Nginx pode oferecer maior performance, segurança e facilidade de uso. + +!!! note "Nota" + Recomendamos colocar servidores HTTP Vapor atrás do Nginx como proxy. + +## Visão Geral + +O que significa usar um proxy para um servidor HTTP? Em resumo, um proxy atua como intermediário entre a internet pública e seu servidor HTTP. As requisições chegam ao proxy e ele as encaminha para o Vapor. + +Uma funcionalidade importante desse proxy intermediário é que ele pode alterar ou até redirecionar as requisições. Por exemplo, o proxy pode exigir que o cliente use TLS (https), limitar a taxa de requisições, ou até servir arquivos públicos sem precisar se comunicar com sua aplicação Vapor. + +![nginx-proxy](https://cloud.githubusercontent.com/assets/1342803/20184965/5d9d588a-a738-11e6-91fe-28c3a4f7e46b.png) + +### Mais Detalhes + +A porta padrão para receber requisições HTTP é a porta `80` (e `443` para HTTPS). Quando você vincula um servidor Vapor à porta `80`, ele receberá e responderá diretamente às requisições HTTP que chegam ao seu servidor. Ao adicionar um proxy como o Nginx, você vincula o Vapor a uma porta interna, como a porta `8080`. + +!!! note "Nota" + Portas maiores que 1024 não requerem `sudo` para vincular. + +Quando o Vapor está vinculado a uma porta diferente de `80` ou `443`, ele não será acessível pela internet externa. Você então vincula o Nginx à porta `80` e o configura para rotear requisições para seu servidor Vapor vinculado à porta `8080` (ou qualquer porta que você tenha escolhido). + +E é isso. Se o Nginx estiver configurado corretamente, você verá sua aplicação Vapor respondendo requisições na porta `80`. O Nginx faz o proxy das requisições e respostas de forma invisível. + +## Instalar Nginx + +O primeiro passo é instalar o Nginx. Uma das grandes vantagens do Nginx é a enorme quantidade de recursos e documentação da comunidade ao seu redor. Por isso, não entraremos em grandes detalhes aqui sobre a instalação do Nginx, já que quase certamente existe um tutorial para sua plataforma, sistema operacional e provedor específicos. + +Tutoriais: + +- [How To Install Nginx on Ubuntu 20.04](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04) +- [How To Install Nginx on Ubuntu 18.04](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-18-04) +- [How to Install Nginx on CentOS 8](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-centos-8) +- [How To Install Nginx on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-16-04) +- [How to Deploy Nginx on Heroku](https://blog.codeship.com/how-to-deploy-nginx-on-heroku/) + +### Gerenciadores de Pacotes + +O Nginx pode ser instalado através de gerenciadores de pacotes no Linux. + +#### Ubuntu + +```sh +sudo apt-get update +sudo apt-get install nginx +``` + +#### CentOS e Amazon Linux + +```sh +sudo yum install nginx +``` + +#### Fedora + +```sh +sudo dnf install nginx +``` + +### Verificar Instalação + +Verifique se o Nginx foi instalado corretamente acessando o endereço IP do seu servidor em um navegador + +``` +http://server_domain_name_or_IP +``` + +### Serviço + +O serviço pode ser iniciado ou parado. + +```sh +sudo service nginx stop +sudo service nginx start +sudo service nginx restart +``` + +## Iniciando o Vapor + +O Nginx pode ser iniciado e parado com os comandos `sudo service nginx ...`. Você precisará de algo similar para iniciar e parar seu servidor Vapor. + +Existem várias formas de fazer isso, e elas dependem de qual plataforma você está fazendo o deploy. Confira as instruções do [Supervisor](supervisor.md) para adicionar comandos para iniciar e parar sua aplicação Vapor. + +## Configurar Proxy + +Os arquivos de configuração para sites habilitados podem ser encontrados em `/etc/nginx/sites-enabled/`. + +Crie um novo arquivo ou copie o template de exemplo de `/etc/nginx/sites-available/` para começar. + +Aqui está um exemplo de arquivo de configuração para um projeto Vapor chamado `Hello` no diretório home. + +```sh +server { + server_name hello.com; + listen 80; + + root /home/vapor/Hello/Public/; + + location @proxy { + proxy_pass http://127.0.0.1:8080; + proxy_pass_header Server; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_connect_timeout 3s; + proxy_read_timeout 10s; + } +} +``` + +Este arquivo de configuração assume que o projeto `Hello` está vinculado à porta `8080` quando iniciado no modo de produção. + +### Servindo Arquivos + +O Nginx também pode servir arquivos públicos sem precisar consultar sua aplicação Vapor. Isso pode melhorar a performance ao liberar o processo do Vapor para outras tarefas sob carga pesada. + +```sh +server { + ... + + # Serve todos os arquivos públicos/estáticos via nginx e então redireciona para o Vapor o restante + location / { + try_files $uri @proxy; + } + + location @proxy { + ... + } +} +``` + +### TLS + +Adicionar TLS é relativamente simples desde que os certificados tenham sido gerados corretamente. Para gerar certificados TLS gratuitamente, confira o [Let's Encrypt](https://letsencrypt.org/getting-started/). + +```sh +server { + ... + + listen 443 ssl; + + ssl_certificate /etc/letsencrypt/live/hello.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/hello.com/privkey.pem; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_dhparam /etc/ssl/certs/dhparam.pem; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:50m; + ssl_stapling on; + ssl_stapling_verify on; + add_header Strict-Transport-Security max-age=15768000; + + ... + + location @proxy { + ... + } +} +``` + +A configuração acima contém opções relativamente rígidas para TLS com Nginx. Algumas das configurações aqui não são obrigatórias, mas aumentam a segurança. diff --git a/docs/deploy/supervisor.pt.md b/docs/deploy/supervisor.pt.md new file mode 100644 index 000000000..584dbff76 --- /dev/null +++ b/docs/deploy/supervisor.pt.md @@ -0,0 +1,72 @@ +# Supervisor + +[Supervisor](http://supervisord.org) é um sistema de controle de processos que facilita iniciar, parar e reiniciar sua aplicação Vapor. + +## Instalar + +O Supervisor pode ser instalado através de gerenciadores de pacotes no Linux. + +### Ubuntu + +```sh +sudo apt-get update +sudo apt-get install supervisor +``` + +### CentOS e Amazon Linux + +```sh +sudo yum install supervisor +``` + +### Fedora + +```sh +sudo dnf install supervisor +``` + +## Configurar + +Cada aplicação Vapor no seu servidor deve ter seu próprio arquivo de configuração. Para um projeto de exemplo `Hello`, o arquivo de configuração estaria localizado em `/etc/supervisor/conf.d/hello.conf` + +```sh +[program:hello] +command=/home/vapor/hello/.build/release/App serve --env production +directory=/home/vapor/hello/ +user=vapor +stdout_logfile=/var/log/supervisor/%(program_name)s-stdout.log +stderr_logfile=/var/log/supervisor/%(program_name)s-stderr.log +``` + +Como especificado no nosso arquivo de configuração, o projeto `Hello` está localizado na pasta home do usuário `vapor`. Certifique-se de que `directory` aponte para o diretório raiz do seu projeto onde o arquivo `Package.swift` está. + +A flag `--env production` desabilitará o logging detalhado. + +### Ambiente + +Você pode exportar variáveis para sua aplicação Vapor com o supervisor. Para exportar múltiplos valores de ambiente, coloque-os todos em uma linha. De acordo com a [documentação do Supervisor](http://supervisord.org/configuration.html#program-x-section-values): + +> Valores contendo caracteres não alfanuméricos devem ser citados (ex: KEY="val:123",KEY2="val,456"). Caso contrário, citar os valores é opcional, mas recomendado. + +```sh +environment=PORT=8123,OUTROVALOR="/algum/outro/caminho" +``` + +Variáveis exportadas podem ser usadas no Vapor usando `Environment.get` + +```swift +let port = Environment.get("PORT") +``` + +## Iniciar + +Agora você pode carregar e iniciar sua aplicação. + +```sh +supervisorctl reread +supervisorctl add hello +supervisorctl start hello +``` + +!!! note "Nota" + O comando `add` pode já ter iniciado sua aplicação. diff --git a/docs/deploy/systemd.pt.md b/docs/deploy/systemd.pt.md new file mode 100644 index 000000000..4ec11ed7a --- /dev/null +++ b/docs/deploy/systemd.pt.md @@ -0,0 +1,67 @@ +# Systemd + +Systemd é o gerenciador de sistema e serviços padrão na maioria das distribuições Linux. Geralmente já vem instalado por padrão, então nenhuma instalação é necessária nas distribuições Swift suportadas. + +## Configurar + +Cada aplicação Vapor no seu servidor deve ter seu próprio arquivo de serviço. Para um projeto de exemplo `Hello`, o arquivo de configuração estaria localizado em `/etc/systemd/system/hello.service`. Este arquivo deve se parecer com o seguinte: + +```sh +[Unit] +Description=Hello +Requires=network.target +After=network.target + +[Service] +Type=simple +User=vapor +Group=vapor +Restart=always +RestartSec=3 +WorkingDirectory=/home/vapor/hello +ExecStart=/home/vapor/hello/.build/release/App serve --env production +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=vapor-hello + +[Install] +WantedBy=multi-user.target +``` + +Como especificado no nosso arquivo de configuração, o projeto `Hello` está localizado na pasta home do usuário `vapor`. Certifique-se de que `WorkingDirectory` aponte para o diretório raiz do seu projeto onde o arquivo `Package.swift` está. + +A flag `--env production` desabilitará o logging detalhado. + +### Ambiente + +Você pode exportar variáveis de duas formas via systemd. Criando um arquivo de ambiente com todas as variáveis definidas: + +```sh +EnvironmentFile=/caminho/do/arquivo/de/ambiente1 +EnvironmentFile=/caminho/do/arquivo/de/ambiente2 +``` + + +Ou você pode adicioná-las diretamente ao arquivo de serviço em `[service]`: + +```sh +Environment="PORT=8123" +Environment="OUTROVALOR=/algum/outro/caminho" +``` +Variáveis exportadas podem ser usadas no Vapor usando `Environment.get` + +```swift +let port = Environment.get("PORT") +``` + +## Iniciar + +Agora você pode carregar, habilitar, iniciar, parar e reiniciar sua aplicação executando os seguintes comandos como root. + +```sh +systemctl daemon-reload +systemctl enable hello +systemctl start hello +systemctl stop hello +systemctl restart hello +``` diff --git a/docs/fluent/advanced.pt.md b/docs/fluent/advanced.pt.md new file mode 100644 index 000000000..2058a7673 --- /dev/null +++ b/docs/fluent/advanced.pt.md @@ -0,0 +1,194 @@ +# Avançado + +O Fluent se esforça para criar uma API geral e agnóstica de banco de dados para trabalhar com seus dados. Isso torna mais fácil aprender o Fluent independentemente de qual driver de banco de dados você está usando. Criar APIs generalizadas também pode fazer com que trabalhar com seu banco de dados pareça mais natural em Swift. + +No entanto, você pode precisar usar um recurso do seu driver de banco de dados subjacente que ainda não é suportado pelo Fluent. Este guia cobre padrões avançados e APIs no Fluent que funcionam apenas com determinados bancos de dados. + +## SQL + +Todos os drivers de banco de dados SQL do Fluent são construídos sobre o [SQLKit](https://github.com/vapor/sql-kit). Esta implementação SQL geral é fornecida com o Fluent no módulo `FluentSQL`. + +### SQL Database + +Qualquer `Database` do Fluent pode ser convertida para um `SQLDatabase`. Isso inclui `req.db`, `app.db`, o `database` passado para `Migration`, etc. + +```swift +import FluentSQL + +if let sql = req.db as? SQLDatabase { + // O driver de banco de dados subjacente é SQL. + let planets = try await sql.raw("SELECT * FROM planets").all(decoding: Planet.self) +} else { + // O driver de banco de dados subjacente _não_ é SQL. +} +``` + +Esta conversão só funcionará se o driver de banco de dados subjacente for um banco de dados SQL. Saiba mais sobre os métodos do `SQLDatabase` no [README do SQLKit](https://github.com/vapor/sql-kit). + +### Banco de Dados SQL Específico + +Você também pode converter para bancos de dados SQL específicos importando o driver. + +```swift +import FluentPostgresDriver + +if let postgres = req.db as? PostgresDatabase { + // O driver de banco de dados subjacente é PostgreSQL. + postgres.simpleQuery("SELECT * FROM planets").all() +} else { + // O banco de dados subjacente _não_ é PostgreSQL. +} +``` + +No momento da escrita, os seguintes drivers SQL são suportados. + +|Banco de Dados|Driver|Biblioteca| +|-|-|-| +|`PostgresDatabase`|[vapor/fluent-postgres-driver](https://github.com/vapor/fluent-postgres-driver)|[vapor/postgres-nio](https://github.com/vapor/postgres-nio)| +|`MySQLDatabase`|[vapor/fluent-mysql-driver](https://github.com/vapor/fluent-mysql-driver)|[vapor/mysql-nio](https://github.com/vapor/mysql-nio)| +|`SQLiteDatabase`|[vapor/fluent-sqlite-driver](https://github.com/vapor/fluent-sqlite-driver)|[vapor/sqlite-nio](https://github.com/vapor/sqlite-nio)| + +Visite o README da biblioteca para mais informações sobre as APIs específicas de cada banco de dados. + +### SQL Custom + +Quase todos os tipos de query e schema do Fluent suportam um caso `.custom`. Isso permite que você utilize recursos do banco de dados que o Fluent ainda não suporta. + +```swift +import FluentPostgresDriver + +let query = Planet.query(on: req.db) +if req.db is PostgresDatabase { + // ILIKE suportado. + query.filter(\.$name, .custom("ILIKE"), "earth") +} else { + // ILIKE não suportado. + query.group(.or) { or in + or.filter(\.$name == "earth").filter(\.$name == "Earth") + } +} +query.all() +``` + +Bancos de dados SQL suportam tanto `String` quanto `SQLExpression` em todos os casos `.custom`. O módulo `FluentSQL` fornece métodos de conveniência para casos de uso comuns. + +```swift +import FluentSQL + +let query = Planet.query(on: req.db) +if req.db is SQLDatabase { + // O driver de banco de dados subjacente é SQL. + query.filter(.sql(raw: "LOWER(name) = 'earth'")) +} else { + // O driver de banco de dados subjacente _não_ é SQL. +} +``` + +Abaixo está um exemplo de `.custom` via a conveniência `.sql(raw:)` sendo usado com o schema builder. + +```swift +import FluentSQL + +let builder = database.schema("planets").id() +if database is MySQLDatabase { + // O driver de banco de dados subjacente é MySQL. + builder.field("name", .sql(raw: "VARCHAR(64)"), .required) +} else { + // O driver de banco de dados subjacente _não_ é MySQL. + builder.field("name", .string, .required) +} +builder.create() +``` + +## MongoDB + +Fluent MongoDB é uma integração entre [Fluent](../fluent/overview.md) e o driver [MongoKitten](https://github.com/OpenKitten/MongoKitten/). Ele aproveita o forte sistema de tipos do Swift e a interface agnóstica de banco de dados do Fluent usando MongoDB. + +O identificador mais comum no MongoDB é ObjectId. Você pode usar isso no seu projeto usando `@ID(custom: .id)`. +Se precisar usar os mesmos models com SQL, não use `ObjectId`. Use `UUID` em vez disso. + +```swift +final class User: Model { + // Nome da tabela ou coleção. + static let schema = "users" + + // Identificador único para este User. + // Neste caso, ObjectId é usado + // O Fluent recomenda usar UUID por padrão, porém ObjectId também é suportado + @ID(custom: .id) + var id: ObjectId? + + // O endereço de email do User + @Field(key: "email") + var email: String + + // A senha do User armazenada como hash BCrypt + @Field(key: "password") + var passwordHash: String + + // Cria uma nova instância vazia de User, para uso pelo Fluent + init() { } + + // Cria um novo User com todas as propriedades definidas. + init(id: ObjectId? = nil, email: String, passwordHash: String, profile: Profile) { + self.id = id + self.email = email + self.passwordHash = passwordHash + self.profile = profile + } +} +``` + +### Modelagem de Dados + +No MongoDB, Models são definidos da mesma forma que em qualquer outro ambiente Fluent. A principal diferença entre bancos de dados SQL e MongoDB está nas relações e arquitetura. + +Em ambientes SQL, é muito comum criar tabelas de junção para relações entre duas entidades. No MongoDB, no entanto, um array pode ser usado para armazenar identificadores relacionados. Devido ao design do MongoDB, é mais eficiente e prático projetar seus models com estruturas de dados aninhadas. + +### Dados Flexíveis + +Você pode adicionar dados flexíveis no MongoDB, mas este código não funcionará em ambientes SQL. +Para criar armazenamento de dados arbitrários agrupados, você pode usar `Document`. + +```swift +@Field(key: "document") +var document: Document +``` + +O Fluent não pode suportar queries com tipagem estrita nesses valores. Você pode usar um key path com notação de ponto na sua query. +Isso é aceito no MongoDB para acessar valores aninhados. + +```swift +Something.query(on: db).filter("document.key", .equal, 5).first() +``` +### Uso de expressões regulares + +Você pode consultar o MongoDB usando o caso `.custom()`, passando uma expressão regular. O [MongoDB](https://www.mongodb.com/docs/manual/reference/operator/query/regex/) aceita expressões regulares compatíveis com Perl. + +Por exemplo, você pode consultar caracteres insensíveis a maiúsculas e minúsculas no campo `name`: + +```swift +import FluentMongoDriver + +var queryDocument = Document() +queryDocument["name"]["$regex"] = "e" +queryDocument["name"]["$options"] = "i" + +let planets = try Planet.query(on: req.db).filter(.custom(queryDocument)).all() +``` + +Isso retornará planetas contendo 'e' e 'E'. Você também pode criar qualquer outra RegEx complexa aceita pelo MongoDB. + +### Acesso Direto + +Para acessar a instância `MongoDatabase` diretamente, converta a instância do banco de dados para `MongoDatabaseRepresentable` da seguinte forma: + +```swift +guard let db = req.db as? MongoDatabaseRepresentable else { + throw Abort(.internalServerError) +} + +let mongodb = db.raw +``` + +A partir daqui você pode usar todas as APIs do MongoKitten. diff --git a/docs/fluent/migration.pt.md b/docs/fluent/migration.pt.md new file mode 100644 index 000000000..4b259b63b --- /dev/null +++ b/docs/fluent/migration.pt.md @@ -0,0 +1,96 @@ +# Migrações + +Migrations são como um sistema de controle de versão para seu banco de dados. Cada migration define uma alteração no banco de dados e como desfazê-la. Ao modificar seu banco de dados através de migrations, você cria uma maneira consistente, testável e compartilhável de evoluir seus bancos de dados ao longo do tempo. + +```swift +// Um exemplo de migration. +struct MyMigration: Migration { + func prepare(on database: any Database) -> EventLoopFuture { + // Faz uma alteração no banco de dados. + } + + func revert(on database: any Database) -> EventLoopFuture { + // Desfaz a alteração feita em `prepare`, se possível. + } +} +``` + +Se estiver usando `async`/`await`, você deve implementar o protocolo `AsyncMigration`: + +```swift +struct MyMigration: AsyncMigration { + func prepare(on database: any Database) async throws { + // Faz uma alteração no banco de dados. + } + + func revert(on database: any Database) async throws { + // Desfaz a alteração feita em `prepare`, se possível. + } +} +``` + +O método `prepare` é onde você faz alterações no `Database` fornecido. Essas podem ser alterações no schema do banco de dados, como adicionar ou remover uma tabela ou coleção, campo ou constraint. Também podem modificar o conteúdo do banco de dados, como criar novas instâncias de models, atualizar valores de campos ou fazer limpeza. + +O método `revert` é onde você desfaz essas alterações, se possível. Poder desfazer migrations pode tornar a prototipagem e os testes mais fáceis. Também fornece um plano de backup caso um deploy em produção não saia como planejado. + +## Registrar + +Migrations são registradas na sua aplicação usando `app.migrations`. + +```swift +import Fluent +import Vapor + +app.migrations.add(MyMigration()) +``` + +Você pode adicionar uma migration a um banco de dados específico usando o parâmetro `to`, caso contrário o banco de dados padrão será usado. + +```swift +app.migrations.add(MyMigration(), to: .myDatabase) +``` + +Migrations devem ser listadas em ordem de dependência. Por exemplo, se `MigrationB` depende de `MigrationA`, ela deve ser adicionada a `app.migrations` em segundo lugar. + +## Migrate + +Para migrar seu banco de dados, execute o comando `migrate`. + +```sh +swift run App migrate +``` + +Você também pode executar este [comando pelo Xcode](../advanced/commands.md#xcode). O comando migrate verificará o banco de dados para ver se alguma nova migration foi registrada desde a última execução. Se houver novas migrations, ele pedirá confirmação antes de executá-las. + +### Revert + +Para desfazer uma migration no seu banco de dados, execute `migrate` com a flag `--revert`. + +```sh +swift run App migrate --revert +``` + +O comando verificará o banco de dados para ver qual lote de migrations foi executado por último e pedirá confirmação antes de revertê-las. + +### Auto Migrate + +Se você deseja que as migrations sejam executadas automaticamente antes de executar outros comandos, você pode passar a flag `--auto-migrate`. + +```sh +swift run App serve --auto-migrate +``` + +Você também pode fazer isso programaticamente. + +```swift +try app.autoMigrate().wait() + +// ou +try await app.autoMigrate() +``` + +Ambas as opções também existem para reverter: `--auto-revert` e `app.autoRevert()`. + +## Próximos Passos + +Dê uma olhada nos guias do [schema builder](schema.md) e do [query builder](query.md) para mais informações sobre o que colocar dentro das suas migrations. diff --git a/docs/fluent/model.pt.md b/docs/fluent/model.pt.md new file mode 100644 index 000000000..4db88311d --- /dev/null +++ b/docs/fluent/model.pt.md @@ -0,0 +1,598 @@ +# Modelos + +Models representam dados armazenados em tabelas ou coleções no seu banco de dados. Models possuem um ou mais campos que armazenam valores codable. Todos os models possuem um identificador único. Property wrappers são usados para denotar identificadores, campos e relações. + +Abaixo está um exemplo de um model simples com um campo. Note que models não descrevem o schema completo do banco de dados, como constraints, índices e foreign keys. Schemas são definidos em [migrations](migration.md). Models são focados em representar os dados armazenados nos seus schemas de banco de dados. + +```swift +final class Planet: Model { + // Nome da tabela ou coleção. + static let schema = "planets" + + // Identificador único para este Planet. + @ID(key: .id) + var id: UUID? + + // O nome do Planet. + @Field(key: "name") + var name: String + + // Cria um novo Planet vazio. + init() { } + + // Cria um novo Planet com todas as propriedades definidas. + init(id: UUID? = nil, name: String) { + self.id = id + self.name = name + } +} +``` + +## Schema + +Todos os models requerem uma propriedade `schema` estática e somente leitura. Esta string referencia o nome da tabela ou coleção que este model representa. + +```swift +final class Planet: Model { + // Nome da tabela ou coleção. + static let schema = "planets" +} +``` + +Ao consultar este model, os dados serão buscados e armazenados no schema chamado `"planets"`. + +!!! tip + O nome do schema é tipicamente o nome da classe pluralizado e em minúsculas. + +## Identifier + +Todos os models devem ter uma propriedade `id` definida usando o property wrapper `@ID`. Este campo identifica unicamente instâncias do seu model. + +```swift +final class Planet: Model { + // Identificador único para este Planet. + @ID(key: .id) + var id: UUID? +} +``` + +Por padrão, a propriedade `@ID` deve usar a chave especial `.id` que resolve para uma chave apropriada para o driver de banco de dados subjacente. Para SQL é `"id"` e para NoSQL é `"_id"`. + +O `@ID` também deve ser do tipo `UUID`. Este é o único tipo de valor de identificador atualmente suportado por todos os drivers de banco de dados. O Fluent gerará automaticamente novos identificadores UUID quando models forem criados. + +`@ID` tem um valor opcional pois models não salvos podem ainda não ter um identificador. Para obter o identificador ou lançar um erro, use `requireID`. + +```swift +let id = try planet.requireID() +``` + +### Exists + +`@ID` possui uma propriedade `exists` que representa se o model existe no banco de dados ou não. Quando você inicializa um model, o valor é `false`. Após salvar um model ou quando você busca um model do banco de dados, o valor é `true`. Esta propriedade é mutável. + +```swift +if planet.$id.exists { + // Este model existe no banco de dados. +} +``` + +### Custom Identifier + +O Fluent suporta chaves e tipos de identificador personalizados usando a sobrecarga `@ID(custom:)`. + +```swift +final class Planet: Model { + // Identificador único para este Planet. + @ID(custom: "foo") + var id: Int? +} +``` + +O exemplo acima usa um `@ID` com chave personalizada `"foo"` e tipo de identificador `Int`. Isso é compatível com bancos de dados SQL usando chaves primárias auto-incrementáveis, mas não é compatível com NoSQL. + +`@ID`s personalizados permitem que o usuário especifique como o identificador deve ser gerado usando o parâmetro `generatedBy`. + +```swift +@ID(custom: "foo", generatedBy: .user) +``` + +O parâmetro `generatedBy` suporta os seguintes casos: + +|Generated By|Descrição| +|-|-| +|`.user`|Espera-se que a propriedade `@ID` seja definida antes de salvar um novo model.| +|`.random`|O tipo de valor do `@ID` deve conformar a `RandomGeneratable`.| +|`.database`|Espera-se que o banco de dados gere um valor ao salvar.| + +Se o parâmetro `generatedBy` for omitido, o Fluent tentará inferir um caso apropriado com base no tipo de valor do `@ID`. Por exemplo, `Int` terá como padrão a geração `.database`, a menos que especificado de outra forma. + +## Initializer + +Models devem ter um método inicializador vazio. + +```swift +final class Planet: Model { + // Cria um novo Planet vazio. + init() { } +} +``` + +O Fluent requer este método internamente para inicializar models retornados por queries. Ele também é usado para reflection. + +Você pode querer adicionar um inicializador de conveniência ao seu model que aceite todas as propriedades. + +```swift +final class Planet: Model { + // Cria um novo Planet com todas as propriedades definidas. + init(id: UUID? = nil, name: String) { + self.id = id + self.name = name + } +} +``` + +Usar inicializadores de conveniência torna mais fácil adicionar novas propriedades ao model no futuro. + +## Field + +Models podem ter zero ou mais propriedades `@Field` para armazenar dados. + +```swift +final class Planet: Model { + // O nome do Planet. + @Field(key: "name") + var name: String +} +``` + +Campos requerem que a chave do banco de dados seja explicitamente definida. Não é necessário que seja a mesma que o nome da propriedade. + +!!! tip + O Fluent recomenda usar `snake_case` para chaves de banco de dados e `camelCase` para nomes de propriedades. + +Valores de campo podem ser qualquer tipo que conforme a `Codable`. Armazenar estruturas aninhadas e arrays em `@Field` é suportado, mas as operações de filtragem são limitadas. Veja [`@Group`](#group) para uma alternativa. + +Para campos que contêm um valor opcional, use `@OptionalField`. + +```swift +@OptionalField(key: "tag") +var tag: String? +``` + +!!! warning + Um campo não-opcional que tem um property observer `willSet` que referencia seu valor atual ou um property observer `didSet` que referencia seu `oldValue` resultará em um erro fatal. + +## Relations + +Models podem ter zero ou mais propriedades de relação referenciando outros models como `@Parent`, `@Children` e `@Siblings`. Saiba mais sobre relações na seção de [relações](relations.md). + +## Timestamp + +`@Timestamp` é um tipo especial de `@Field` que armazena um `Foundation.Date`. Timestamps são definidos automaticamente pelo Fluent de acordo com o trigger escolhido. + +```swift +final class Planet: Model { + // Quando este Planet foi criado. + @Timestamp(key: "created_at", on: .create) + var createdAt: Date? + + // Quando este Planet foi atualizado pela última vez. + @Timestamp(key: "updated_at", on: .update) + var updatedAt: Date? +} +``` + +`@Timestamp` suporta os seguintes triggers. + +|Trigger|Descrição| +|-|-| +|`.create`|Definido quando uma nova instância de model é salva no banco de dados.| +|`.update`|Definido quando uma instância existente de model é salva no banco de dados.| +|`.delete`|Definido quando um model é deletado do banco de dados. Veja [soft delete](#soft-delete).| + +O valor de data do `@Timestamp` é opcional e deve ser definido como `nil` ao inicializar um novo model. + +### Formato de Timestamp + +Por padrão, `@Timestamp` usará uma codificação `datetime` eficiente baseada no seu driver de banco de dados. Você pode personalizar como o timestamp é armazenado no banco de dados usando o parâmetro `format`. + +```swift +// Armazena um timestamp formatado em ISO 8601 representando +// quando este model foi atualizado pela última vez. +@Timestamp(key: "updated_at", on: .update, format: .iso8601) +var updatedAt: Date? +``` + +Note que a migration associada para este exemplo `.iso8601` requer armazenamento em formato `.string`. + +```swift +.field("updated_at", .string) +``` + +Formatos de timestamp disponíveis estão listados abaixo. + +|Formato|Descrição|Tipo| +|-|-|-| +|`.default`|Usa codificação `datetime` eficiente específica do banco de dados.|Date| +|`.iso8601`|String [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). Suporta parâmetro `withMilliseconds`.|String| +|`.unix`|Segundos desde a época Unix incluindo fração.|Double| + +Você pode acessar o valor bruto do timestamp diretamente usando a propriedade `timestamp`. + +```swift +// Define manualmente o valor do timestamp neste +// @Timestamp formatado em ISO 8601. +model.$updatedAt.timestamp = "2020-06-03T16:20:14+00:00" +``` + +### Soft Delete + +Adicionar um `@Timestamp` que usa o trigger `.delete` ao seu model habilitará soft-deletion. + +```swift +final class Planet: Model { + // Quando este Planet foi deletado. + @Timestamp(key: "deleted_at", on: .delete) + var deletedAt: Date? +} +``` + +Models soft-deleted ainda existem no banco de dados após a exclusão, mas não serão retornados em queries. + +!!! tip + Você pode definir manualmente um timestamp on delete para uma data no futuro. Isso pode ser usado como data de expiração. + +Para forçar a remoção de um model soft-deletable do banco de dados, use o parâmetro `force` em `delete`. + +```swift +// Deleta do banco de dados mesmo se o model +// for soft deletable. +model.delete(force: true, on: database) +``` + +Para restaurar um model soft-deleted, use o método `restore`. + +```swift +// Limpa o timestamp de exclusão permitindo que este +// model seja retornado em queries. +model.restore(on: database) +``` + +Para incluir models soft-deleted em uma query, use `withDeleted`. + +```swift +// Busca todos os planetas incluindo os soft deleted. +Planet.query(on: database).withDeleted().all() +``` + +## Enum + +`@Enum` é um tipo especial de `@Field` para armazenar tipos representáveis por string como enums nativos do banco de dados. Enums nativos do banco de dados fornecem uma camada adicional de segurança de tipos ao seu banco de dados e podem ser mais performáticos que enums brutos. + +```swift +// Enum Codable representável por string para tipos de animais. +enum Animal: String, Codable { + case dog, cat +} + +final class Pet: Model { + // Armazena o tipo de animal como um enum nativo do banco de dados. + @Enum(key: "type") + var type: Animal +} +``` + +Apenas tipos que conformam a `RawRepresentable` onde `RawValue` é `String` são compatíveis com `@Enum`. Enums baseados em `String` atendem a este requisito por padrão. + +Para armazenar um enum opcional, use `@OptionalEnum`. + +O banco de dados deve ser preparado para lidar com enums via uma migration. Veja [enum](schema.md#enum) para mais informações. + +### Raw Enums + +Qualquer enum baseado em um tipo `Codable`, como `String` ou `Int`, pode ser armazenado em `@Field`. Ele será armazenado no banco de dados como o valor bruto. + +## Group + +`@Group` permite que você armazene um grupo aninhado de campos como uma única propriedade no seu model. Diferentemente de structs Codable armazenadas em um `@Field`, os campos em um `@Group` são consultáveis. O Fluent consegue isso armazenando `@Group` como uma estrutura plana no banco de dados. + +Para usar um `@Group`, primeiro defina a estrutura aninhada que você gostaria de armazenar usando o protocolo `Fields`. Isso é muito similar a `Model` exceto que nenhum identificador ou nome de schema é necessário. Você pode armazenar muitas propriedades aqui que `Model` suporta como `@Field`, `@Enum` ou até outro `@Group`. + +```swift +// Um pet com nome e tipo de animal. +final class Pet: Fields { + // O nome do pet. + @Field(key: "name") + var name: String + + // O tipo de pet. + @Field(key: "type") + var type: String + + // Cria um novo Pet vazio. + init() { } +} +``` + +Após criar a definição de fields, você pode usá-la como valor de uma propriedade `@Group`. + +```swift +final class User: Model { + // O pet aninhado do usuário. + @Group(key: "pet") + var pet: Pet +} +``` + +Os campos de um `@Group` são acessíveis via sintaxe de ponto. + +```swift +let user: User = ... +print(user.pet.name) // String +``` + +Você pode consultar campos aninhados normalmente usando sintaxe de ponto nos property wrappers. + +```swift +User.query(on: database).filter(\.$pet.$name == "Zizek").all() +``` + +No banco de dados, `@Group` é armazenado como uma estrutura plana com chaves unidas por `_`. Abaixo está um exemplo de como `User` ficaria no banco de dados. + +|id|name|pet_name|pet_type| +|-|-|-|-| +|1|Tanner|Zizek|Cat| +|2|Logan|Runa|Dog| + +## Codable + +Models conformam a `Codable` por padrão. Isso significa que você pode usar seus models com a [API de conteúdo](../basics/content.md) do Vapor adicionando conformidade ao protocolo `Content`. + +```swift +extension Planet: Content { } + +app.get("planets") { req async throws in + // Retorna um array de todos os planetas. + try await Planet.query(on: req.db).all() +} +``` + +Ao serializar para / de `Codable`, propriedades de model usarão seus nomes de variável em vez de chaves. Relações serializarão como estruturas aninhadas e quaisquer dados carregados via eager loading serão incluídos. + +!!! info + Recomendamos que para quase todos os casos você use um DTO em vez de um model para suas respostas e corpos de requisição da API. Veja [Data Transfer Object](#data-transfer-object) para mais informações. + +### Data Transfer Object + +A conformidade padrão do Model a `Codable` pode tornar o uso simples e a prototipagem mais fáceis. No entanto, ela expõe as informações subjacentes do banco de dados à API. Isso geralmente não é desejável tanto do ponto de vista de segurança — retornar campos sensíveis como o hash de senha de um usuário é uma má ideia — quanto do ponto de vista de usabilidade. Torna difícil alterar o schema do banco de dados sem quebrar a API, aceitar ou retornar dados em um formato diferente, ou adicionar ou remover campos da API. + +Para a maioria dos casos você deve usar um DTO, ou data transfer object, em vez de um model (também conhecido como domain transfer object). Um DTO é um tipo `Codable` separado representando a estrutura de dados que você gostaria de codificar ou decodificar. Estes desacoplam sua API do schema do banco de dados e permitem que você faça alterações nos seus models sem quebrar a API pública do seu app, tenha diferentes versões e torne sua API mais agradável de usar para seus clients. + +Assuma o seguinte model `User` nos próximos exemplos. + +```swift +// Model de usuário resumido para referência. +final class User: Model { + @ID(key: .id) + var id: UUID? + + @Field(key: "first_name") + var firstName: String + + @Field(key: "last_name") + var lastName: String +} +``` + +Um caso de uso comum para DTOs é na implementação de requisições `PATCH`. Essas requisições incluem apenas valores para campos que devem ser atualizados. Tentar decodificar um `Model` diretamente de tal requisição falharia se qualquer um dos campos obrigatórios estivesse faltando. No exemplo abaixo, você pode ver um DTO sendo usado para decodificar dados da requisição e atualizar um model. + +```swift +// Estrutura da requisição PATCH /users/:id. +struct PatchUser: Decodable { + var firstName: String? + var lastName: String? +} + +app.patch("users", ":id") { req async throws -> User in + // Decodifica os dados da requisição. + let patch = try req.content.decode(PatchUser.self) + // Busca o usuário desejado no banco de dados. + guard let user = try await User.find(req.parameters.get("id"), on: req.db) else { + throw Abort(.notFound) + } + // Se o primeiro nome foi fornecido, atualiza. + if let firstName = patch.firstName { + user.firstName = firstName + } + // Se o novo sobrenome foi fornecido, atualiza. + if let lastName = patch.lastName { + user.lastName = lastName + } + // Salva o usuário e o retorna. + try await user.save(on: req.db) + return user +} +``` + +Outro caso de uso comum para DTOs é personalizar o formato das respostas da sua API. O exemplo abaixo mostra como um DTO pode ser usado para adicionar um campo computado a uma resposta. + +```swift +// Estrutura da resposta GET /users. +struct GetUser: Content { + var id: UUID + var name: String +} + +app.get("users") { req async throws -> [GetUser] in + // Busca todos os usuários do banco de dados. + let users = try await User.query(on: req.db).all() + return try users.map { user in + // Converte cada usuário para o tipo de retorno GET. + try GetUser( + id: user.requireID(), + name: "\(user.firstName) \(user.lastName)" + ) + } +} +``` + +Outro caso de uso comum é ao lidar com relações, como relações parent ou children. Veja a [documentação de Parent](relations.md##encoding-and-decoding-of-parents) para um exemplo de como usar um DTO para facilitar a decodificação de um model com uma relação `@Parent`. + +Mesmo que a estrutura do DTO seja idêntica à conformidade `Codable` do model, tê-lo como um tipo separado pode ajudar a manter projetos grandes organizados. Se você precisar fazer uma alteração nas propriedades dos seus models, não precisa se preocupar em quebrar a API pública do seu app. Você também pode considerar colocar seus DTOs em um pacote separado que pode ser compartilhado com consumidores da sua API e adicionar conformidade a `Content` no seu app Vapor. + +## Alias + +O protocolo `ModelAlias` permite que você identifique unicamente um model sendo adicionado via join múltiplas vezes em uma query. Para mais informações, veja [joins](query.md#join). + +## Save + +Para salvar um model no banco de dados, use o método `save(on:)`. + +```swift +planet.save(on: database) +``` + +Este método chamará `create` ou `update` internamente dependendo de se o model já existe no banco de dados. + +### Create + +Você pode chamar o método `create` para salvar um novo model no banco de dados. + +```swift +let planet = Planet(name: "Earth") +planet.create(on: database) +``` + +`create` também está disponível em um array de models. Isso salva todos os models no banco de dados em um único batch / query. + +```swift +// Exemplo de criação em batch. +[earth, mars].create(on: database) +``` + +!!! warning + Models usando [`@ID(custom:)`](#custom-identifier) com o gerador `.database` (geralmente `Int`s auto-incrementáveis) não terão seus identificadores recém-criados acessíveis após batch create. Para situações onde você precisa acessar os identificadores, chame `create` em cada model. + +Para criar um array de models separadamente, use `map` + `flatten`. + +```swift +[earth, mars].map { $0.create(on: database) } + .flatten(on: database.eventLoop) +``` + +Se estiver usando `async`/`await`, pode usar: + +```swift +await withThrowingTaskGroup(of: Void.self) { taskGroup in + [earth, mars].forEach { model in + taskGroup.addTask { try await model.create(on: database) } + } +} +``` + +### Update + +Você pode chamar o método `update` para salvar um model que foi buscado do banco de dados. + +```swift +guard let planet = try await Planet.find(..., on: database) else { + throw Abort(.notFound) +} +planet.name = "Earth" +try await planet.update(on: database) +``` + +Para atualizar um array de models, use `map` + `flatten`. + +```swift +[earth, mars].map { $0.update(on: database) } + .flatten(on: database.eventLoop) + +// TOOD +``` + +## Query + +Models expõem um método estático `query(on:)` que retorna um query builder. + +```swift +Planet.query(on: database).all() +``` + +Saiba mais sobre queries na seção de [query](query.md). + +## Find + +Models possuem um método estático `find(_:on:)` para buscar uma instância de model por identificador. + +```swift +Planet.find(req.parameters.get("id"), on: database) +``` + +Este método retorna `nil` se nenhum model com aquele identificador for encontrado. + +## Lifecycle + +Model middleware permitem que você se conecte aos eventos de ciclo de vida do seu model. Os seguintes eventos de ciclo de vida são suportados. + +|Método|Descrição| +|-|-| +|`create`|Executa antes de um model ser criado.| +|`update`|Executa antes de um model ser atualizado.| +|`delete(force:)`|Executa antes de um model ser deletado.| +|`softDelete`|Executa antes de um model ser soft deleted.| +|`restore`|Executa antes de um model ser restaurado (oposto de soft delete).| + +Model middleware são declarados usando o protocolo `ModelMiddleware` ou `AsyncModelMiddleware`. Todos os métodos de ciclo de vida possuem uma implementação padrão, então você só precisa implementar os métodos que necessita. Cada método aceita o model em questão, uma referência ao banco de dados e a próxima ação na cadeia. O middleware pode escolher retornar antecipadamente, retornar um future que falhou ou chamar a próxima ação para continuar normalmente. + +Usando esses métodos, você pode realizar ações tanto antes quanto depois do evento específico ser completado. Realizar ações após o evento ser completado pode ser feito mapeando o future retornado pelo próximo responder. + +```swift +// Exemplo de middleware que capitaliza nomes. +struct PlanetMiddleware: ModelMiddleware { + func create(model: Planet, on db: Database, next: AnyModelResponder) -> EventLoopFuture { + // O model pode ser alterado aqui antes de ser criado. + model.name = model.name.capitalized() + return next.create(model, on: db).map { + // Uma vez que o planeta foi criado, o código + // aqui será executado. + print ("Planet \(model.name) was created") + } + } +} +``` + +ou se estiver usando `async`/`await`: + +```swift +struct PlanetMiddleware: AsyncModelMiddleware { + func create(model: Planet, on db: Database, next: AnyAsyncModelResponder) async throws { + // O model pode ser alterado aqui antes de ser criado. + model.name = model.name.capitalized() + try await next.create(model, on: db) + // Uma vez que o planeta foi criado, o código + // aqui será executado. + print ("Planet \(model.name) was created") + } +} +``` + +Uma vez que você criou seu middleware, pode habilitá-lo usando `app.databases.middleware`. + +```swift +// Exemplo de configuração de model middleware. +app.databases.middleware.use(PlanetMiddleware(), on: .psql) +``` + +## Database Space + +O Fluent suporta a definição de um espaço para um Model, o que permite o particionamento de models individuais do Fluent entre schemas do PostgreSQL, bancos de dados MySQL e múltiplos bancos de dados SQLite anexados. MongoDB não suporta espaços no momento desta escrita. Para colocar um model em um espaço diferente do padrão, adicione uma nova propriedade estática ao model: + +```swift +public static let schema = "planets" +public static let space: String? = "mirror_universe" + +// ... +``` + +O Fluent usará isso ao construir todas as queries de banco de dados. diff --git a/docs/fluent/overview.pt.md b/docs/fluent/overview.pt.md new file mode 100644 index 000000000..cdb31fd4e --- /dev/null +++ b/docs/fluent/overview.pt.md @@ -0,0 +1,606 @@ +# Fluent + +Fluent é um framework [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) para Swift. Ele aproveita o forte sistema de tipos do Swift para fornecer uma interface fácil de usar para seu banco de dados. O uso do Fluent gira em torno da criação de tipos de model que representam estruturas de dados no seu banco de dados. Esses models são então usados para realizar operações de criação, leitura, atualização e exclusão em vez de escrever queries brutas. + +## Configuração + +Ao criar um projeto usando `vapor new`, responda "yes" para incluir o Fluent e escolha qual driver de banco de dados deseja usar. Isso adicionará automaticamente as dependências ao seu novo projeto, bem como código de configuração de exemplo. + +### Projeto Existente + +Se você tem um projeto existente ao qual deseja adicionar o Fluent, precisará adicionar duas dependências ao seu [package](../getting-started/spm.md): + +- [vapor/fluent](https://github.com/vapor/fluent)@4.0.0 +- Um (ou mais) driver(s) Fluent de sua escolha + +```swift +.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), +.package(url: "https://github.com/vapor/fluent--driver.git", from: ), +``` + +```swift +.target(name: "App", dependencies: [ + .product(name: "Fluent", package: "fluent"), + .product(name: "FluentDriver", package: "fluent--driver"), + .product(name: "Vapor", package: "vapor"), +]), +``` + +Uma vez que os pacotes são adicionados como dependências, você pode configurar seus bancos de dados usando `app.databases` em `configure.swift`. + +```swift +import Fluent +import FluentDriver + +app.databases.use(, as: ) +``` + +Cada um dos drivers Fluent abaixo possui instruções mais específicas para configuração. + +### Drivers + +O Fluent atualmente possui quatro drivers oficialmente suportados. Você pode pesquisar no GitHub pela tag [`fluent-driver`](https://github.com/topics/fluent-driver) para uma lista completa de drivers de banco de dados Fluent oficiais e de terceiros. + +#### PostgreSQL + +PostgreSQL é um banco de dados SQL open source, compatível com padrões. É facilmente configurável na maioria dos provedores de hospedagem em nuvem. Este é o driver de banco de dados **recomendado** pelo Fluent. + +Para usar PostgreSQL, adicione as seguintes dependências ao seu package. + +```swift +.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") +``` + +```swift +.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver") +``` + +Uma vez que as dependências são adicionadas, configure as credenciais do banco de dados com o Fluent usando `app.databases.use` em `configure.swift`. + +```swift +import Fluent +import FluentPostgresDriver + +app.databases.use( + .postgres( + configuration: .init( + hostname: "localhost", + username: "vapor", + password: "vapor", + database: "vapor", + tls: .disable + ) + ), + as: .psql +) +``` + +Você também pode analisar as credenciais a partir de uma string de conexão do banco de dados. + +```swift +try app.databases.use(.postgres(url: ""), as: .psql) +``` + +#### SQLite + +SQLite é um banco de dados SQL open source, embutido. Sua natureza simplista o torna um ótimo candidato para prototipagem e testes. + +Para usar SQLite, adicione as seguintes dependências ao seu package. + +```swift +.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0") +``` + +```swift +.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver") +``` + +Uma vez que as dependências são adicionadas, configure o banco de dados com o Fluent usando `app.databases.use` em `configure.swift`. + +```swift +import Fluent +import FluentSQLiteDriver + +app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite) +``` + +Você também pode configurar o SQLite para armazenar o banco de dados de forma efêmera na memória. + +```swift +app.databases.use(.sqlite(.memory), as: .sqlite) +``` + +Se usar um banco de dados em memória, certifique-se de configurar o Fluent para migrar automaticamente usando `--auto-migrate` ou execute `app.autoMigrate()` após adicionar as migrations. + +```swift +app.migrations.add(CreateTodo()) +try app.autoMigrate().wait() +// ou +try await app.autoMigrate() +``` + +!!! tip + A configuração do SQLite habilita automaticamente constraints de foreign key em todas as conexões criadas, mas não altera as configurações de foreign key no próprio banco de dados. Deletar registros em um banco de dados diretamente pode violar constraints e triggers de foreign key. + +#### MySQL + +MySQL é um banco de dados SQL open source popular. Está disponível em muitos provedores de hospedagem em nuvem. Este driver também suporta MariaDB. + +Para usar MySQL, adicione as seguintes dependências ao seu package. + +```swift +.package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.0.0") +``` + +```swift +.product(name: "FluentMySQLDriver", package: "fluent-mysql-driver") +``` + +Uma vez que as dependências são adicionadas, configure as credenciais do banco de dados com o Fluent usando `app.databases.use` em `configure.swift`. + +```swift +import Fluent +import FluentMySQLDriver + +app.databases.use(.mysql(hostname: "localhost", username: "vapor", password: "vapor", database: "vapor"), as: .mysql) +``` + +Você também pode analisar as credenciais a partir de uma string de conexão do banco de dados. + +```swift +try app.databases.use(.mysql(url: ""), as: .mysql) +``` + +Para configurar uma conexão local sem certificado SSL envolvido, você deve desabilitar a verificação de certificado. Pode ser necessário fazer isso, por exemplo, ao conectar a um banco de dados MySQL 8 no Docker. + +```swift +var tls = TLSConfiguration.makeClientConfiguration() +tls.certificateVerification = .none + +app.databases.use(.mysql( + hostname: "localhost", + username: "vapor", + password: "vapor", + database: "vapor", + tlsConfiguration: tls +), as: .mysql) +``` + +!!! warning + Não desabilite a verificação de certificado em produção. Você deve fornecer um certificado à `TLSConfiguration` para verificar. + +#### MongoDB + +MongoDB é um banco de dados NoSQL sem schema popular, projetado para programadores. O driver suporta todos os provedores de hospedagem em nuvem e instalações auto-hospedadas a partir da versão 3.4 em diante. + +!!! note + Este driver é alimentado por um client MongoDB criado e mantido pela comunidade chamado [MongoKitten](https://github.com/OpenKitten/MongoKitten). O MongoDB mantém um client oficial, [mongo-swift-driver](https://github.com/mongodb/mongo-swift-driver), junto com uma integração Vapor, [mongodb-vapor](https://github.com/mongodb/mongodb-vapor). + +Para usar MongoDB, adicione as seguintes dependências ao seu package. + +```swift +.package(url: "https://github.com/vapor/fluent-mongo-driver.git", from: "1.0.0"), +``` + +```swift +.product(name: "FluentMongoDriver", package: "fluent-mongo-driver") +``` + +Uma vez que as dependências são adicionadas, configure as credenciais do banco de dados com o Fluent usando `app.databases.use` em `configure.swift`. + +Para conectar, passe uma string de conexão no formato padrão de [URI de conexão](https://docs.mongodb.com/docs/manual/reference/connection-string/) do MongoDB. + +```swift +import Fluent +import FluentMongoDriver + +try app.databases.use(.mongo(connectionString: ""), as: .mongo) +``` + +## Models + +Models representam estruturas de dados fixas no seu banco de dados, como tabelas ou coleções. Models possuem um ou mais campos que armazenam valores codable. Todos os models também possuem um identificador único. Property wrappers são usados para denotar identificadores e campos, assim como mapeamentos mais complexos mencionados posteriormente. Veja o seguinte model que representa uma galáxia. + +```swift +final class Galaxy: Model { + // Nome da tabela ou coleção. + static let schema = "galaxies" + + // Identificador único para esta Galaxy. + @ID(key: .id) + var id: UUID? + + // O nome da Galaxy. + @Field(key: "name") + var name: String + + // Cria uma nova Galaxy vazia. + init() { } + + // Cria uma nova Galaxy com todas as propriedades definidas. + init(id: UUID? = nil, name: String) { + self.id = id + self.name = name + } +} +``` + +Para criar um novo model, crie uma nova classe em conformidade com `Model`. + +!!! tip + É recomendado marcar classes de model como `final` para melhorar o desempenho e simplificar requisitos de conformidade. + +O primeiro requisito do protocolo `Model` é a string estática `schema`. + +```swift +static let schema = "galaxies" +``` + +Esta propriedade diz ao Fluent a qual tabela ou coleção o model corresponde. Pode ser uma tabela que já existe no banco de dados ou uma que você criará com uma [migration](#migrations). O schema é geralmente `snake_case` e plural. + +### Identifier + +O próximo requisito é um campo identificador chamado `id`. + +```swift +@ID(key: .id) +var id: UUID? +``` + +Este campo deve usar o property wrapper `@ID`. O Fluent recomenda usar `UUID` e a chave de campo especial `.id` pois isso é compatível com todos os drivers do Fluent. + +Se quiser usar uma chave ou tipo de ID personalizado, use a sobrecarga [`@ID(custom:)`](model.md#custom-identifier). + +### Fields + +Após o identificador ser adicionado, você pode adicionar quantos campos quiser para armazenar informações adicionais. Neste exemplo, o único campo adicional é o nome da galáxia. + +```swift +@Field(key: "name") +var name: String +``` + +Para campos simples, o property wrapper `@Field` é usado. Como `@ID`, o parâmetro `key` especifica o nome do campo no banco de dados. Isso é especialmente útil para casos onde a convenção de nomenclatura do banco de dados pode ser diferente da do Swift, por exemplo, usando `snake_case` em vez de `camelCase`. + +Em seguida, todos os models requerem um init vazio. Isso permite que o Fluent crie novas instâncias do model. + +```swift +init() { } +``` + +Por fim, você pode adicionar um init de conveniência para seu model que defina todas as suas propriedades. + +```swift +init(id: UUID? = nil, name: String) { + self.id = id + self.name = name +} +``` + +Usar inits de conveniência é especialmente útil se você adicionar novas propriedades ao seu model, pois você pode obter erros de compilação se o método init mudar. + +## Migrations + +Se seu banco de dados usa schemas pré-definidos, como bancos de dados SQL, você precisará de uma migration para preparar o banco de dados para seu model. Migrations também são úteis para popular bancos de dados com dados. Para criar uma migration, defina um novo tipo em conformidade com o protocolo `Migration` ou `AsyncMigration`. Veja a seguinte migration para o model `Galaxy` definido anteriormente. + +```swift +struct CreateGalaxy: AsyncMigration { + // Prepara o banco de dados para armazenar models Galaxy. + func prepare(on database: Database) async throws { + try await database.schema("galaxies") + .id() + .field("name", .string) + .create() + } + + // Opcionalmente reverte as alterações feitas no método prepare. + func revert(on database: Database) async throws { + try await database.schema("galaxies").delete() + } +} +``` + +O método `prepare` é usado para preparar o banco de dados para armazenar models `Galaxy`. + +### Schema + +Neste método, `database.schema(_:)` é usado para criar um novo `SchemaBuilder`. Um ou mais `field`s são então adicionados ao builder antes de chamar `create()` para criar o schema. + +Cada campo adicionado ao builder tem um nome, tipo e constraints opcionais. + +```swift +field(, , ) +``` + +Existe um método de conveniência `id()` para adicionar propriedades `@ID` usando os padrões recomendados pelo Fluent. + +Reverter a migration desfaz quaisquer alterações feitas no método prepare. Neste caso, isso significa deletar o schema de Galaxy. + +Uma vez que a migration é definida, você deve informar o Fluent sobre ela adicionando-a a `app.migrations` em `configure.swift`. + +```swift +app.migrations.add(CreateGalaxy()) +``` + +### Migrate + +Para executar migrations, chame `swift run App migrate` pela linha de comando ou adicione `migrate` como argumento ao scheme App do Xcode. + + +``` +$ swift run App migrate +Migrate Command: Prepare +The following migration(s) will be prepared: ++ CreateGalaxy on default +Would you like to continue? +y/n> y +Migration successful +``` + +## Querying + +Agora que você criou um model e migrou seu banco de dados com sucesso, está pronto para fazer sua primeira query. + +### All + +Veja a seguinte rota que retornará um array de todas as galáxias no banco de dados. + +```swift +app.get("galaxies") { req async throws in + try await Galaxy.query(on: req.db).all() +} +``` + +Para retornar um Galaxy diretamente em uma closure de rota, adicione conformidade a `Content`. + +```swift +final class Galaxy: Model, Content { + ... +} +``` + +`Galaxy.query` é usado para criar um novo query builder para o model. `req.db` é uma referência ao banco de dados padrão para sua aplicação. Por fim, `all()` retorna todos os models armazenados no banco de dados. + +Se você compilar e executar o projeto e requisitar `GET /galaxies`, você deverá ver um array vazio retornado. Vamos adicionar uma rota para criar uma nova galáxia. + +### Create + + +Seguindo a convenção RESTful, use o endpoint `POST /galaxies` para criar uma nova galáxia. Como models são codable, você pode decodificar uma galáxia diretamente do corpo da requisição. + +```swift +app.post("galaxies") { req -> EventLoopFuture in + let galaxy = try req.content.decode(Galaxy.self) + return galaxy.create(on: req.db) + .map { galaxy } +} +``` + +!!! seealso + Veja [Content → Overview](../basics/content.md) para mais informações sobre decodificação de corpos de requisição. + +Uma vez que você tem uma instância do model, chamar `create(on:)` salva o model no banco de dados. Isso retorna um `EventLoopFuture` que sinaliza que o salvamento foi concluído. Uma vez que o salvamento é completado, retorne o model recém-criado usando `map`. + +Se estiver usando `async`/`await`, você pode escrever seu código assim: + +```swift +app.post("galaxies") { req async throws -> Galaxy in + let galaxy = try req.content.decode(Galaxy.self) + try await galaxy.create(on: req.db) + return galaxy +} +``` + +Neste caso, a versão async não retorna nada, mas retornará uma vez que o salvamento for concluído. + +Compile e execute o projeto e envie a seguinte requisição. + +```http +POST /galaxies HTTP/1.1 +content-length: 21 +content-type: application/json + +{ + "name": "Milky Way" +} +``` + +Você deverá receber o model criado de volta com um identificador como resposta. + +```json +{ + "id": ..., + "name": "Milky Way" +} +``` + +Agora, se você consultar `GET /galaxies` novamente, deverá ver a galáxia recém-criada retornada no array. + + +## Relations + +O que são galáxias sem estrelas! Vamos dar uma olhada rápida nos poderosos recursos relacionais do Fluent adicionando uma relação um-para-muitos entre `Galaxy` e um novo model `Star`. + +```swift +final class Star: Model, Content { + // Nome da tabela ou coleção. + static let schema = "stars" + + // Identificador único para esta Star. + @ID(key: .id) + var id: UUID? + + // O nome da Star. + @Field(key: "name") + var name: String + + // Referência à Galaxy em que esta Star está. + @Parent(key: "galaxy_id") + var galaxy: Galaxy + + // Cria uma nova Star vazia. + init() { } + + // Cria uma nova Star com todas as propriedades definidas. + init(id: UUID? = nil, name: String, galaxyID: UUID) { + self.id = id + self.name = name + self.$galaxy.id = galaxyID + } +} +``` + +### Parent + +O novo model `Star` é muito similar a `Galaxy` exceto por um novo tipo de campo: `@Parent`. + +```swift +@Parent(key: "galaxy_id") +var galaxy: Galaxy +``` + +A propriedade parent é um campo que armazena o identificador de outro model. O model que mantém a referência é chamado de "filho" e o model referenciado é chamado de "pai". Este tipo de relação também é conhecido como "um-para-muitos". O parâmetro `key` da propriedade especifica o nome do campo que deve ser usado para armazenar a chave do pai no banco de dados. + +No método init, o identificador do pai é definido usando `$galaxy`. + +```swift +self.$galaxy.id = galaxyID +``` + +Ao prefixar o nome da propriedade parent com `$`, você acessa o property wrapper subjacente. Isso é necessário para obter acesso ao `@Field` interno que armazena o valor real do identificador. + +!!! seealso + Confira a proposta do Swift Evolution para property wrappers para mais informações: [[SE-0258] Property Wrappers](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0258-property-wrappers.md) + +Em seguida, crie uma migration para preparar o banco de dados para lidar com `Star`. + + +```swift +struct CreateStar: AsyncMigration { + // Prepara o banco de dados para armazenar models Star. + func prepare(on database: Database) async throws { + try await database.schema("stars") + .id() + .field("name", .string) + .field("galaxy_id", .uuid, .references("galaxies", "id")) + .create() + } + + // Opcionalmente reverte as alterações feitas no método prepare. + func revert(on database: Database) async throws { + try await database.schema("stars").delete() + } +} +``` + +Isso é basicamente o mesmo que a migration de galaxy exceto pelo campo adicional para armazenar o identificador da galáxia pai. + +```swift +field("galaxy_id", .uuid, .references("galaxies", "id")) +``` + +Este campo especifica uma constraint opcional dizendo ao banco de dados que o valor do campo referencia o campo "id" no schema "galaxies". Isso também é conhecido como foreign key e ajuda a garantir a integridade dos dados. + +Uma vez que a migration é criada, adicione-a a `app.migrations` após a migration `CreateGalaxy`. + +```swift +app.migrations.add(CreateGalaxy()) +app.migrations.add(CreateStar()) +``` + +Como migrations são executadas em ordem, e `CreateStar` referencia o schema galaxies, a ordenação é importante. Por fim, [execute as migrations](#migrate) para preparar o banco de dados. + +Adicione uma rota para criar novas estrelas. + +```swift +app.post("stars") { req async throws -> Star in + let star = try req.content.decode(Star.self) + try await star.create(on: req.db) + return star +} +``` + +Crie uma nova estrela referenciando a galáxia criada anteriormente usando a seguinte requisição HTTP. + +```http +POST /stars HTTP/1.1 +content-length: 36 +content-type: application/json + +{ + "name": "Sun", + "galaxy": { + "id": ... + } +} +``` + +Você deverá ver a estrela recém-criada retornada com um identificador único. + +```json +{ + "id": ..., + "name": "Sun", + "galaxy": { + "id": ... + } +} +``` + +### Children + +Agora vamos ver como você pode utilizar o recurso de eager-loading do Fluent para retornar automaticamente as estrelas de uma galáxia na rota `GET /galaxies`. Adicione a seguinte propriedade ao model `Galaxy`. + +```swift +// Todas as Stars nesta Galaxy. +@Children(for: \.$galaxy) +var stars: [Star] +``` + +O property wrapper `@Children` é o inverso de `@Parent`. Ele recebe um key-path para o campo `@Parent` do filho como argumento `for`. Seu valor é um array de filhos, pois zero ou mais models filhos podem existir. Nenhuma alteração na migration de galaxy é necessária, pois todas as informações necessárias para esta relação estão armazenadas em `Star`. + +### Eager Load + +Agora que a relação está completa, você pode usar o método `with` no query builder para buscar e serializar automaticamente a relação galaxy-star. + +```swift +app.get("galaxies") { req in + try await Galaxy.query(on: req.db).with(\.$stars).all() +} +``` + +Um key-path para a relação `@Children` é passado para `with` para dizer ao Fluent para carregar automaticamente esta relação em todos os models resultantes. Compile e execute e envie outra requisição para `GET /galaxies`. Agora você deverá ver as estrelas incluídas automaticamente na resposta. + +```json +[ + { + "id": ..., + "name": "Milky Way", + "stars": [ + { + "id": ..., + "name": "Sun", + "galaxy": { + "id": ... + } + } + ] + } +] +``` + +## Query Logging + +Os drivers Fluent registram o SQL gerado no nível de log debug. Alguns drivers, como FluentPostgreSQL, permitem que isso seja configurado ao configurar o banco de dados. + +Para definir o nível de log, em **configure.swift** (ou onde você configura sua aplicação) adicione: + +```swift +app.logger.logLevel = .debug +``` + +Isso define o nível de log para debug. Quando você compilar e executar seu app novamente, as declarações SQL geradas pelo Fluent serão registradas no console. + +## Próximos passos + +Parabéns por criar seus primeiros models e migrations e realizar operações básicas de criação e leitura. Para informações mais aprofundadas sobre todos esses recursos, confira suas respectivas seções no guia do Fluent. diff --git a/docs/fluent/query.pt.md b/docs/fluent/query.pt.md new file mode 100644 index 000000000..bc59a239c --- /dev/null +++ b/docs/fluent/query.pt.md @@ -0,0 +1,382 @@ +# Consultas + +A API de query do Fluent permite que você crie, leia, atualize e delete models do banco de dados. Ela suporta filtragem de resultados, joins, chunking, agregações e mais. + +```swift +// Um exemplo da API de query do Fluent. +let planets = try await Planet.query(on: database) + .filter(\.$type == .gasGiant) + .sort(\.$name) + .with(\.$star) + .all() +``` + +Query builders estão vinculados a um único tipo de model e podem ser criados usando o método estático [`query`](model.md#query). Eles também podem ser criados passando o tipo do model para o método `query` em um objeto database. + +```swift +// Também cria um query builder. +database.query(Planet.self) +``` + +!!! note + Você deve importar `import Fluent` no arquivo com suas queries para que o compilador possa ver as funções auxiliares do Fluent. + +## All + +O método `all()` retorna um array de models. + +```swift +// Busca todos os planetas. +let planets = try await Planet.query(on: database).all() +``` + +O método `all` também suporta buscar apenas um único campo do conjunto de resultados. + +```swift +// Busca todos os nomes dos planetas. +let names = try await Planet.query(on: database).all(\.$name) +``` + +### First + +O método `first()` retorna um único model opcional. Se a query resultar em mais de um model, apenas o primeiro é retornado. Se a query não tiver resultados, `nil` é retornado. + +```swift +// Busca o primeiro planeta chamado Earth. +let earth = try await Planet.query(on: database) + .filter(\.$name == "Earth") + .first() +``` + +!!! tip + Se estiver usando `EventLoopFuture`s, este método pode ser combinado com [`unwrap(or:)`](../basics/errors.md#abort) para retornar um model não-opcional ou lançar um erro. + +## Filter + +O método `filter` permite que você restrinja os models incluídos no conjunto de resultados. Existem várias sobrecargas para este método. + +### Value Filter + +O `filter` mais comumente usado aceita uma expressão de operador com um valor. + +```swift +// Um exemplo de filtragem por valor de campo. +Planet.query(on: database).filter(\.$type == .gasGiant) +``` + +Essas expressões de operador aceitam um key path de campo no lado esquerdo e um valor no lado direito. O valor fornecido deve corresponder ao tipo de valor esperado do campo e é vinculado à query resultante. Expressões de filtro são fortemente tipadas, permitindo o uso da sintaxe de ponto inicial. + +Abaixo está uma lista de todos os operadores de valor suportados. + +|Operador|Descrição| +|-|-| +|`==`|Igual a.| +|`!=`|Diferente de.| +|`>=`|Maior ou igual a.| +|`>`|Maior que.| +|`<`|Menor que.| +|`<=`|Menor ou igual a.| + +### Field Filter + +O método `filter` suporta comparar dois campos. + +```swift +// Todos os usuários com o mesmo primeiro nome e sobrenome. +User.query(on: database) + .filter(\.$firstName == \.$lastName) +``` + +Filtros de campo suportam os mesmos operadores que [filtros de valor](#value-filter). + +### Subset Filter + +O método `filter` suporta verificar se o valor de um campo existe em um conjunto de valores fornecido. + +```swift +// Todos os planetas do tipo gas giant ou small rocky. +Planet.query(on: database) + .filter(\.$type ~~ [.gasGiant, .smallRocky]) +``` + +O conjunto de valores fornecido pode ser qualquer `Collection` do Swift cujo tipo `Element` corresponda ao tipo de valor do campo. + +Abaixo está uma lista de todos os operadores de subset suportados. + +|Operador|Descrição| +|-|-| +|`~~`|Valor no conjunto.| +|`!~`|Valor não está no conjunto.| + +### Contains Filter + +O método `filter` suporta verificar se o valor de um campo string contém uma determinada substring. + +```swift +// Todos os planetas cujo nome começa com a letra M +Planet.query(on: database) + .filter(\.$name =~ "M") +``` + +Esses operadores estão disponíveis apenas em campos com valores string. + +Abaixo está uma lista de todos os operadores de contains suportados. + +|Operador|Descrição| +|-|-| +|`~~`|Contém substring.| +|`!~`|Não contém substring.| +|`=~`|Corresponde ao prefixo.| +|`!=~`|Não corresponde ao prefixo.| +|`~=`|Corresponde ao sufixo.| +|`!~=`|Não corresponde ao sufixo.| + +### Group + +Por padrão, todos os filtros adicionados a uma query devem corresponder. O query builder suporta a criação de um grupo de filtros onde apenas um filtro precisa corresponder. + +```swift +// Todos os planetas cujo nome é Earth ou Mars +Planet.query(on: database).group(.or) { group in + group.filter(\.$name == "Earth").filter(\.$name == "Mars") +}.all() +``` + +O método `group` suporta combinar filtros por lógica `and` ou `or`. Esses grupos podem ser aninhados indefinidamente. Filtros de nível superior podem ser considerados como estando em um grupo `and`. + +## Aggregate + +O query builder suporta vários métodos para realizar cálculos em um conjunto de valores como contagem ou média. + +```swift +// Número de planetas no banco de dados. +Planet.query(on: database).count() +``` + +Todos os métodos de agregação além de `count` requerem que um key path para um campo seja passado. + +```swift +// Menor nome ordenado alfabeticamente. +Planet.query(on: database).min(\.$name) +``` + +Abaixo está uma lista de todos os métodos de agregação disponíveis. + +|Agregação|Descrição| +|-|-| +|`count`|Número de resultados.| +|`sum`|Soma dos valores dos resultados.| +|`average`|Média dos valores dos resultados.| +|`min`|Valor mínimo dos resultados.| +|`max`|Valor máximo dos resultados.| + +Todos os métodos de agregação exceto `count` retornam o tipo de valor do campo como resultado. `count` sempre retorna um inteiro. + +## Chunk + +O query builder suporta retornar um conjunto de resultados como chunks separados. Isso ajuda você a controlar o uso de memória ao lidar com leituras grandes do banco de dados. + +```swift +// Busca todos os planetas em chunks de no máximo 64 por vez. +Planet.query(on: self.database).chunk(max: 64) { planets in + // Processa o chunk de planetas. +} +``` + +A closure fornecida será chamada zero ou mais vezes dependendo do número total de resultados. Cada item retornado é um `Result` contendo o model ou um erro retornado ao tentar decodificar a entrada do banco de dados. + +## Field + +Por padrão, todos os campos de um model serão lidos do banco de dados por uma query. Você pode escolher selecionar apenas um subconjunto dos campos de um model usando o método `field`. + +```swift +// Seleciona apenas os campos id e name do planeta +Planet.query(on: database) + .field(\.$id).field(\.$name) + .all() +``` + +Quaisquer campos do model não selecionados durante uma query estarão em estado não inicializado. Tentar acessar campos não inicializados diretamente resultará em um erro fatal. Para verificar se o valor de um campo do model está definido, use a propriedade `value`. + +```swift +if let name = planet.$name.value { + // O nome foi buscado. +} else { + // O nome não foi buscado. + // Acessar `planet.name` falhará. +} +``` + +## Unique + +O método `unique` do query builder faz com que apenas resultados distintos (sem duplicatas) sejam retornados. + +```swift +// Retorna todos os primeiros nomes únicos dos usuários. +User.query(on: database).unique().all(\.$firstName) +``` + +`unique` é especialmente útil ao buscar um único campo com `all`. No entanto, você também pode selecionar múltiplos campos usando o método [`field`](#field). Como identificadores de model são sempre únicos, você deve evitar selecioná-los ao usar `unique`. + +## Range + +Os métodos `range` do query builder permitem que você escolha um subconjunto dos resultados usando ranges do Swift. + +```swift +// Busca os primeiros 5 planetas. +Planet.query(on: self.database) + .range(..<5) +``` + +Valores de range são inteiros sem sinal começando em zero. Saiba mais sobre [ranges do Swift](https://developer.apple.com/documentation/swift/range). + +```swift +// Pula os primeiros 2 resultados. +.range(2...) +``` + +## Join + +O método `join` do query builder permite que você inclua os campos de outro model no seu conjunto de resultados. Mais de um model pode ser adicionado via join à sua query. + +```swift +// Busca todos os planetas com uma estrela chamada Sun. +Planet.query(on: database) + .join(Star.self, on: \Planet.$star.$id == \Star.$id) + .filter(Star.self, \.$name == "Sun") + .all() +``` + +O parâmetro `on` aceita uma expressão de igualdade entre dois campos. Um dos campos já deve existir no conjunto de resultados atual. O outro campo deve existir no model sendo adicionado via join. Esses campos devem ter o mesmo tipo de valor. + +A maioria dos métodos do query builder, como `filter` e `sort`, suportam models adicionados via join. Se um método suporta models via join, ele aceitará o tipo do model como primeiro parâmetro. + +```swift +// Ordena pelo campo "name" do model Star via join. +.sort(Star.self, \.$name) +``` + +Queries que usam joins ainda retornarão um array do model base. Para acessar o model adicionado via join, use o método `joined`. + +```swift +// Acessando o model via join a partir do resultado da query. +let planet: Planet = ... +let star = try planet.joined(Star.self) +``` + +### Model Alias + +Model aliases permitem que você adicione o mesmo model a uma query múltiplas vezes via join. Para declarar um model alias, crie um ou mais tipos em conformidade com `ModelAlias`. + +```swift +// Exemplo de model aliases. +final class HomeTeam: ModelAlias { + static let name = "home_teams" + let model = Team() +} +final class AwayTeam: ModelAlias { + static let name = "away_teams" + let model = Team() +} +``` + +Esses tipos referenciam o model sendo usado como alias via a propriedade `model`. Uma vez criados, você pode usar model aliases como models normais em um query builder. + +```swift +// Busca todas as partidas onde o nome do time da casa é Vapor +// e ordena pelo nome do time visitante. +let matches = try await Match.query(on: self.database) + .join(HomeTeam.self, on: \Match.$homeTeam.$id == \HomeTeam.$id) + .join(AwayTeam.self, on: \Match.$awayTeam.$id == \AwayTeam.$id) + .filter(HomeTeam.self, \.$name == "Vapor") + .sort(AwayTeam.self, \.$name) + .all() +``` + +Todos os campos do model são acessíveis através do tipo de model alias via `@dynamicMemberLookup`. + +```swift +// Acessa o model via join a partir do resultado. +let home = try match.joined(HomeTeam.self) +print(home.name) +``` + +## Update + +O query builder suporta atualizar mais de um model por vez usando o método `update`. + +```swift +// Atualiza todos os planetas chamados "Pluto" +Planet.query(on: database) + .set(\.$type, to: .dwarf) + .filter(\.$name == "Pluto") + .update() +``` + +`update` suporta os métodos `set`, `filter` e `range`. + +## Delete + +O query builder suporta deletar mais de um model por vez usando o método `delete`. + +```swift +// Deleta todos os planetas chamados "Vulcan" +Planet.query(on: database) + .filter(\.$name == "Vulcan") + .delete() +``` + +`delete` suporta o método `filter`. + +## Paginate + +A API de query do Fluent suporta paginação automática de resultados usando o método `paginate`. + +```swift +// Exemplo de paginação baseada em requisição. +app.get("planets") { req in + try await Planet.query(on: req.db).paginate(for: req) +} +``` + +O método `paginate(for:)` usará os parâmetros `page` e `per` disponíveis na URI da requisição para retornar o conjunto desejado de resultados. Metadados sobre a página atual e o número total de resultados são incluídos na chave `metadata`. + +```http +GET /planets?page=2&per=5 HTTP/1.1 +``` + +A requisição acima produziria uma resposta estruturada como a seguinte. + +```json +{ + "items": [...], + "metadata": { + "page": 2, + "per": 5, + "total": 8 + } +} +``` + +Números de página começam em `1`. Você também pode fazer uma requisição de página manual. + +```swift +// Exemplo de paginação manual. +.paginate(PageRequest(page: 1, per: 2)) +``` + +## Sort + +Resultados de query podem ser ordenados por valores de campo usando o método `sort`. + +```swift +// Busca planetas ordenados por nome. +Planet.query(on: database).sort(\.$name) +``` + +Ordenações adicionais podem ser adicionadas como fallbacks em caso de empate. Fallbacks serão usados na ordem em que foram adicionados ao query builder. + +```swift +// Busca usuários ordenados por nome. Se dois usuários têm o mesmo nome, ordena por idade. +User.query(on: database).sort(\.$name).sort(\.$age) +``` diff --git a/docs/fluent/relations.pt.md b/docs/fluent/relations.pt.md new file mode 100644 index 000000000..87791fcef --- /dev/null +++ b/docs/fluent/relations.pt.md @@ -0,0 +1,405 @@ +# Relações + +A [API de model](model.md) do Fluent ajuda você a criar e manter referências entre seus models através de relações. Três tipos de relações são suportados: + +- [Parent](#parent) / [Child](#optional-child) (Um-para-um) +- [Parent](#parent) / [Children](#children) (Um-para-muitos) +- [Siblings](#siblings) (Muitos-para-muitos) + +## Parent + +A relação `@Parent` armazena uma referência à propriedade `@ID` de outro model. + +```swift +final class Planet: Model { + // Exemplo de uma relação parent. + @Parent(key: "star_id") + var star: Star +} +``` + +`@Parent` contém um `@Field` chamado `id` que é usado para definir e atualizar a relação. + +```swift +// Define o id da relação parent +earth.$star.id = sun.id +``` + +Por exemplo, o inicializador de `Planet` ficaria assim: + +```swift +init(name: String, starID: Star.IDValue) { + self.name = name + // ... + self.$star.id = starID +} +``` + +O parâmetro `key` define a chave do campo a ser usado para armazenar o identificador do pai. Assumindo que `Star` tem um identificador `UUID`, esta relação `@Parent` é compatível com a seguinte [definição de campo](schema.md#field). + +```swift +.field("star_id", .uuid, .required, .references("star", "id")) +``` + +Note que a constraint [`.references`](schema.md#field-constraint) é opcional. Veja [schema](schema.md) para mais informações. + +### Optional Parent + +A relação `@OptionalParent` armazena uma referência opcional à propriedade `@ID` de outro model. Funciona de forma similar a `@Parent`, mas permite que a relação seja `nil`. + +```swift +final class Planet: Model { + // Exemplo de uma relação parent opcional. + @OptionalParent(key: "star_id") + var star: Star? +} +``` + +A definição de campo é similar à do `@Parent`, exceto que a constraint `.required` deve ser omitida. + +```swift +.field("star_id", .uuid, .references("star", "id")) +``` + +### Codificação e Decodificação de Parents + +Uma coisa a observar ao trabalhar com relações `@Parent` é a maneira como você as envia e recebe. Por exemplo, em JSON, um `@Parent` para um model `Planet` pode ser assim: + +```json +{ + "id": "A616B398-A963-4EC7-9D1D-B1AA8A6F1107", + "star": { + "id": "A1B2C3D4-1234-5678-90AB-CDEF12345678" + } +} +``` + +Note como a propriedade `star` é um objeto em vez do ID que você poderia esperar. Ao enviar o model como body HTTP, ele precisa corresponder a isso para que a decodificação funcione. Por esta razão, recomendamos fortemente usar um DTO para representar o model ao enviá-lo pela rede. Por exemplo: + +```swift +struct PlanetDTO: Content { + var id: UUID? + var name: String + var star: Star.IDValue +} +``` + +Então você pode decodificar o DTO e convertê-lo em um 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) +``` + +O mesmo se aplica ao retornar o model para clients. Seus clients precisam ser capazes de lidar com a estrutura aninhada, ou você precisa converter o model em um DTO antes de retorná-lo. Para mais informações sobre DTOs, veja a [documentação de Model](model.md#data-transfer-object) + +## Optional Child + +A propriedade `@OptionalChild` cria uma relação um-para-um entre os dois models. Ela não armazena nenhum valor no model raiz. + +```swift +final class Planet: Model { + // Exemplo de uma relação child opcional. + @OptionalChild(for: \.$planet) + var governor: Governor? +} +``` + +O parâmetro `for` aceita um key path para uma relação `@Parent` ou `@OptionalParent` que referencia o model raiz. + +Um novo model pode ser adicionado a esta relação usando o método `create`. + +```swift +// Exemplo de adição de um novo model a uma relação. +let jane = Governor(name: "Jane Doe") +try await mars.$governor.create(jane, on: database) +``` + +Isso definirá o id do pai no model filho automaticamente. + +Como esta relação não armazena nenhum valor, nenhuma entrada de schema do banco de dados é necessária para o model raiz. + +A natureza um-para-um da relação deve ser garantida no schema do model filho usando uma constraint `.unique` na coluna que referencia o model pai. + +```swift +try await database.schema(Governor.schema) + .id() + .field("name", .string, .required) + .field("planet_id", .uuid, .required, .references("planets", "id")) + // Exemplo de constraint unique + .unique(on: "planet_id") + .create() +``` +!!! warning + Omitir a constraint unique no campo de ID do pai no schema do filho pode levar a resultados imprevisíveis. + Se não houver constraint de unicidade, a tabela filha pode acabar contendo mais de uma linha filha para qualquer pai dado; neste caso, uma propriedade `@OptionalChild` ainda só será capaz de acessar um filho por vez, sem maneira de controlar qual filho é carregado. Se você pode precisar armazenar múltiplas linhas filhas para qualquer pai dado, use `@Children` em vez disso. + +## Children + +A propriedade `@Children` cria uma relação um-para-muitos entre dois models. Ela não armazena nenhum valor no model raiz. + +```swift +final class Star: Model { + // Exemplo de uma relação children. + @Children(for: \.$star) + var planets: [Planet] +} +``` + +O parâmetro `for` aceita um key path para uma relação `@Parent` ou `@OptionalParent` que referencia o model raiz. Neste caso, estamos referenciando a relação `@Parent` do [exemplo](#parent) anterior. + +Novos models podem ser adicionados a esta relação usando o método `create`. + +```swift +// Exemplo de adição de um novo model a uma relação. +let earth = Planet(name: "Earth") +try await sun.$planets.create(earth, on: database) +``` + +Isso definirá o id do pai no model filho automaticamente. + +Como esta relação não armazena nenhum valor, nenhuma entrada de schema do banco de dados é necessária. + +## Siblings + +A propriedade `@Siblings` cria uma relação muitos-para-muitos entre dois models. Ela faz isso através de um model terciário chamado pivot. + +Vamos dar uma olhada em um exemplo de uma relação muitos-para-muitos entre `Planet` e `Tag`. + +```swift +enum PlanetTagStatus: String, Codable { case accepted, pending } + +// Exemplo de um model pivot. +final class PlanetTag: Model { + static let schema = "planet+tag" + + @ID(key: .id) + var id: UUID? + + @Parent(key: "planet_id") + var planet: Planet + + @Parent(key: "tag_id") + var tag: Tag + + @OptionalField(key: "comments") + var comments: String? + + @OptionalEnum(key: "status") + var status: PlanetTagStatus? + + init() { } + + init(id: UUID? = nil, planet: Planet, tag: Tag, comments: String?, status: PlanetTagStatus?) throws { + self.id = id + self.$planet.id = try planet.requireID() + self.$tag.id = try tag.requireID() + self.comments = comments + self.status = status + } +} +``` + +Qualquer model que inclua pelo menos duas relações `@Parent`, uma para cada model a ser relacionado, pode ser usado como pivot. O model pode conter propriedades adicionais, como seu ID, e pode até conter outras relações `@Parent`. + +Adicionar uma constraint [unique](schema.md#unique) ao model pivot pode ajudar a prevenir entradas redundantes. Veja [schema](schema.md) para mais informações. + +```swift +// Não permite relações duplicadas. +.unique(on: "planet_id", "tag_id") +``` + +Uma vez que o pivot é criado, use a propriedade `@Siblings` para criar a relação. + +```swift +final class Planet: Model { + // Exemplo de uma relação siblings. + @Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag) + public var tags: [Tag] +} +``` + +A propriedade `@Siblings` requer três parâmetros: + +- `through`: O tipo do model pivot. +- `from`: Key path do pivot para a relação pai que referencia o model raiz. +- `to`: Key path do pivot para a relação pai que referencia o model relacionado. + +A propriedade `@Siblings` inversa no model relacionado completa a relação. + +```swift +final class Tag: Model { + // Exemplo de uma relação siblings. + @Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet) + public var planets: [Planet] +} +``` + +### Siblings Attach + +A propriedade `@Siblings` possui métodos para adicionar e remover models da relação. + +Use o método `attach()` para adicionar um único model ou um array de models à relação. Models pivot são criados e salvos automaticamente conforme necessário. Uma closure de callback pode ser especificada para popular propriedades adicionais de cada pivot criado: + +```swift +let earth: Planet = ... +let inhabited: Tag = ... +// Adiciona o model à relação. +try await earth.$tags.attach(inhabited, on: database) +// Popula atributos do pivot ao estabelecer a relação. +try await earth.$tags.attach(inhabited, on: database) { pivot in + pivot.comments = "This is a life-bearing planet." + pivot.status = .accepted +} +// Adiciona múltiplos models com atributos à relação. +let volcanic: Tag = ..., oceanic: Tag = ... +try await earth.$tags.attach([volcanic, oceanic], on: database) { pivot in + pivot.comments = "This planet has a tag named \(pivot.$tag.name)." + pivot.status = .pending +} +``` + +Ao anexar um único model, você pode usar o parâmetro `method` para escolher se a relação deve ser verificada antes de salvar. + +```swift +// Só anexa se a relação ainda não existir. +try await earth.$tags.attach(inhabited, method: .ifNotExists, on: database) +``` + +Use o método `detach` para remover um model da relação. Isso deleta o model pivot correspondente. + +```swift +// Remove o model da relação. +try await earth.$tags.detach(inhabited, on: database) +``` + +Você pode verificar se um model está relacionado ou não usando o método `isAttached`. + +```swift +// Verifica se os models estão relacionados. +earth.$tags.isAttached(to: inhabited) +``` + +## Get + +Use o método `get(on:)` para buscar o valor de uma relação. + +```swift +// Busca todos os planetas do sol. +sun.$planets.get(on: database).map { planets in + print(planets) +} + +// Ou + +let planets = try await sun.$planets.get(on: database) +print(planets) +``` + +Use o parâmetro `reload` para escolher se a relação deve ser buscada novamente do banco de dados caso já tenha sido carregada. + +```swift +try await sun.$planets.get(reload: true, on: database) +``` + +## Query + +Use o método `query(on:)` em uma relação para criar um query builder para os models relacionados. + +```swift +// Busca todos os planetas do sol que têm um nome começando com M. +try await sun.$planets.query(on: database).filter(\.$name =~ "M").all() +``` + +Veja [query](query.md) para mais informações. + +## Eager Loading + +O query builder do Fluent permite que você pré-carregue as relações de um model quando ele é buscado do banco de dados. Isso é chamado de eager loading e permite que você acesse relações sincronamente sem precisar chamar [`get`](#get) primeiro. + +Para fazer eager load de uma relação, passe um key path para a relação ao método `with` no query builder. + +```swift +// Exemplo de eager loading. +Planet.query(on: database).with(\.$star).all().map { planets in + for planet in planets { + // `star` é acessível sincronamente aqui + // pois foi carregado via eager loading. + print(planet.star.name) + } +} + +// Ou + +let planets = try await Planet.query(on: database).with(\.$star).all() +for planet in planets { + // `star` é acessível sincronamente aqui + // pois foi carregado via eager loading. + print(planet.star.name) +} +``` + +No exemplo acima, um key path para a relação [`@Parent`](#parent) chamada `star` é passado para `with`. Isso faz com que o query builder faça uma query adicional depois que todos os planetas são carregados para buscar todas as suas estrelas relacionadas. As estrelas então ficam acessíveis sincronamente via a propriedade `@Parent`. + +Cada relação com eager load requer apenas uma query adicional, não importa quantos models são retornados. Eager loading só é possível com os métodos `all` e `first` do query builder. + + +### Nested Eager Load + +O método `with` do query builder permite que você faça eager load de relações no model sendo consultado. No entanto, você também pode fazer eager load de relações em models relacionados. + +```swift +let planets = try await Planet.query(on: database).with(\.$star) { star in + star.with(\.$galaxy) +}.all() +for planet in planets { + // `star.galaxy` é acessível sincronamente aqui + // pois foi carregado via eager loading. + print(planet.star.galaxy.name) +} +``` + +O método `with` aceita uma closure opcional como segundo parâmetro. Esta closure aceita um eager load builder para a relação escolhida. Não há limite para quão profundamente o eager loading pode ser aninhado. + +## Lazy Eager Loading + +Caso você já tenha recuperado o model pai e queira carregar uma de suas relações, você pode usar o método `get(reload:on:)` para esse propósito. Isso buscará o model relacionado do banco de dados (ou cache, se disponível) e permitirá que ele seja acessado como uma propriedade local. + +```swift +planet.$star.get(on: database).map { + print(planet.star.name) +} + +// Ou + +try await planet.$star.get(on: database) +print(planet.star.name) +``` + +Caso queira garantir que os dados que você recebe não sejam puxados do cache, use o parâmetro `reload:`. + +```swift +try await planet.$star.get(reload: true, on: database) +print(planet.star.name) +``` + +Para verificar se uma relação já foi carregada, use a propriedade `value`. + +```swift +if planet.$star.value != nil { + // A relação foi carregada. + print(planet.star.name) +} else { + // A relação não foi carregada. + // Tentar acessar planet.star falhará. +} +``` + +Se você já tem o model relacionado em uma variável, pode definir a relação manualmente usando a propriedade `value` mencionada acima. + +```swift +planet.$star.value = star +``` + +Isso anexará o model relacionado ao pai como se tivesse sido carregado via eager loading ou lazy loading sem uma query adicional ao banco de dados. diff --git a/docs/fluent/schema.pt.md b/docs/fluent/schema.pt.md new file mode 100644 index 000000000..48f586e52 --- /dev/null +++ b/docs/fluent/schema.pt.md @@ -0,0 +1,426 @@ +# Schema + +A API de schema do Fluent permite que você crie e atualize o schema do seu banco de dados programaticamente. Ela é frequentemente usada em conjunto com [migrations](migration.md) para preparar o banco de dados para uso com [models](model.md). + +```swift +// Um exemplo da API de schema do Fluent +try await database.schema("planets") + .id() + .field("name", .string, .required) + .field("star_id", .uuid, .required, .references("stars", "id")) + .create() +``` + +Para criar um `SchemaBuilder`, use o método `schema` no database. Passe o nome da tabela ou coleção que deseja afetar. Se estiver editando o schema para um model, certifique-se de que este nome corresponda ao [`schema`](model.md#schema) do model. + +## Ações + +A API de schema suporta criação, atualização e exclusão de schemas. Cada ação suporta um subconjunto dos métodos disponíveis da API. + +### Criar + +Chamar `create()` cria uma nova tabela ou coleção no banco de dados. Todos os métodos para definir novos campos e constraints são suportados. Métodos para atualizações ou exclusões são ignorados. + +```swift +// Um exemplo de criação de schema. +try await database.schema("planets") + .id() + .field("name", .string, .required) + .create() +``` + +Se uma tabela ou coleção com o nome escolhido já existir, um erro será lançado. Para ignorar isso, use `.ignoreExisting()`. + +### Atualizar + +Chamar `update()` atualiza uma tabela ou coleção existente no banco de dados. Todos os métodos para criar, atualizar e deletar campos e constraints são suportados. + +```swift +// Um exemplo de atualização de schema. +try await database.schema("planets") + .unique(on: "name") + .deleteField("star_id") + .update() +``` + +### Deletar + +Chamar `delete()` deleta uma tabela ou coleção existente do banco de dados. Nenhum método adicional é suportado. + +```swift +// Um exemplo de exclusão de schema. +database.schema("planets").delete() +``` + +## Field + +Campos podem ser adicionados ao criar ou atualizar um schema. + +```swift +// Adiciona um novo campo +.field("name", .string, .required) +``` + +O primeiro parâmetro é o nome do campo. Isso deve corresponder à chave usada na propriedade do model associado. O segundo parâmetro é o [tipo de dado](#data-type) do campo. Por fim, zero ou mais [constraints](#field-constraint) podem ser adicionadas. + +### Data Type + +Tipos de dados de campo suportados estão listados abaixo. + +|DataType|Tipo Swift| +|-|-| +|`.string`|`String`| +|`.int{8,16,32,64}`|`Int{8,16,32,64}`| +|`.uint{8,16,32,64}`|`UInt{8,16,32,64}`| +|`.bool`|`Bool`| +|`.datetime`|`Date` (recomendado)| +|`.date`|`Date` (omitindo hora do dia)| +|`.float`|`Float`| +|`.double`|`Double`| +|`.data`|`Data`| +|`.uuid`|`UUID`| +|`.dictionary`|Veja [dictionary](#dictionary)| +|`.array`|Veja [array](#array)| +|`.enum`|Veja [enum](#enum)| + +### Field Constraint + +Constraints de campo suportadas estão listadas abaixo. + +|FieldConstraint|Descrição| +|-|-| +|`.required`|Não permite valores `nil`.| +|`.references`|Exige que o valor deste campo corresponda a um valor no schema referenciado. Veja [foreign key](#foreign-key).| +|`.identifier`|Denota a chave primária. Veja [identifier](#identifier).| +|`.sql(SQLColumnConstraintAlgorithm)`|Define qualquer constraint não suportada (ex: `default`). Veja [SQL](#sql) e [SQLColumnConstraintAlgorithm](https://api.vapor.codes/sqlkit/documentation/sqlkit/sqlcolumnconstraintalgorithm/).| + +### Identifier + +Se seu model usa uma propriedade `@ID` padrão, você pode usar o helper `id()` para criar seu campo. Isso usa a chave de campo especial `.id` e o tipo de valor `UUID`. + +```swift +// Adiciona campo para o identificador padrão. +.id() +``` + +Para tipos de identificador personalizados, você precisará especificar o campo manualmente. + +```swift +// Adiciona campo para identificador personalizado. +.field("id", .int, .identifier(auto: true)) +``` + +A constraint `identifier` pode ser usada em um único campo e denota a chave primária. A flag `auto` determina se o banco de dados deve gerar este valor automaticamente. + +### Update Field + +Você pode atualizar o tipo de dado de um campo usando `updateField`. + +```swift +// Atualiza o campo para o tipo de dado `double`. +.updateField("age", .double) +``` + +Veja [advanced](advanced.md#sql) para mais informações sobre atualizações avançadas de schema. + +### Delete Field + +Você pode remover um campo de um schema usando `deleteField`. + +```swift +// Deleta o campo "age". +.deleteField("age") +``` + +## Constraint + +Constraints podem ser adicionadas ao criar ou atualizar um schema. Diferentemente das [constraints de campo](#field-constraint), constraints de nível superior podem afetar múltiplos campos. + +### Unique + +Uma constraint unique exige que não existam valores duplicados em um ou mais campos. + +```swift +// Não permite endereços de email duplicados. +.unique(on: "email") +``` + +Se múltiplos campos forem restringidos, a combinação específica do valor de cada campo deve ser única. + +```swift +// Não permite usuários com o mesmo nome completo. +.unique(on: "first_name", "last_name") +``` + +Para deletar uma constraint unique, use `deleteUnique`. + +```swift +// Remove a constraint de email duplicado. +.deleteUnique(on: "email") +``` + +### Constraint Name + +O Fluent gerará nomes de constraints únicos por padrão. No entanto, você pode querer passar um nome de constraint personalizado. Você pode fazer isso usando o parâmetro `name`. + +```swift +// Não permite endereços de email duplicados. +.unique(on: "email", name: "no_duplicate_emails") +``` + +Para deletar uma constraint nomeada, você deve usar `deleteConstraint(name:)`. + +```swift +// Remove a constraint de email duplicado. +.deleteConstraint(name: "no_duplicate_emails") +``` + +## Foreign Key + +Constraints de foreign key exigem que o valor de um campo corresponda a um dos valores no campo referenciado. Isso é útil para prevenir que dados inválidos sejam salvos. Constraints de foreign key podem ser adicionadas como constraint de campo ou de nível superior. + +Para adicionar uma constraint de foreign key a um campo, use `.references`. + +```swift +// Exemplo de adição de uma constraint de foreign key em campo. +.field("star_id", .uuid, .required, .references("stars", "id")) +``` + +A constraint acima exige que todos os valores no campo "star_id" correspondam a um dos valores no campo "id" de Star. + +Esta mesma constraint pode ser adicionada como constraint de nível superior usando `foreignKey`. + +```swift +// Exemplo de adição de uma constraint de foreign key de nível superior. +.foreignKey("star_id", references: "stars", "id") +``` + +Diferentemente das constraints de campo, constraints de nível superior podem ser adicionadas em uma atualização de schema. Elas também podem ser [nomeadas](#constraint-name). + +Constraints de foreign key suportam ações opcionais `onDelete` e `onUpdate`. + +|ForeignKeyAction|Descrição| +|-|-| +|`.noAction`|Previne violações de foreign key (padrão).| +|`.restrict`|Mesmo que `.noAction`.| +|`.cascade`|Propaga exclusões através de foreign keys.| +|`.setNull`|Define o campo como null se a referência for quebrada.| +|`.setDefault`|Define o campo como padrão se a referência for quebrada.| + +Abaixo está um exemplo usando ações de foreign key. + +```swift +// Exemplo de adição de uma constraint de foreign key de nível superior. +.foreignKey("star_id", references: "stars", "id", onDelete: .cascade) +``` + +!!! warning + Ações de foreign key acontecem exclusivamente no banco de dados, ignorando o Fluent. + Isso significa que coisas como model middleware e soft-delete podem não funcionar corretamente. + +## SQL + +O parâmetro `.sql` permite que você adicione SQL arbitrário ao seu schema. Isso é útil para adicionar constraints ou tipos de dados específicos. +Um caso de uso comum é definir um valor padrão para um campo: + +```swift +.field("active", .bool, .required, .sql(.default(true))) +``` + +ou até um valor padrão para um timestamp: + +```swift +.field("created_at", .datetime, .required, .sql(.default(SQLFunction("now")))) +``` + +## Dictionary + +O tipo de dado dictionary é capaz de armazenar valores de dicionário aninhados. Isso inclui structs que conformam a `Codable` e dicionários Swift com um valor `Codable`. + +!!! note + Os drivers de banco de dados SQL do Fluent armazenam dicionários aninhados em colunas JSON. + +Considere a seguinte struct `Codable`. + +```swift +struct Pet: Codable { + var name: String + var age: Int +} +``` + +Como esta struct `Pet` é `Codable`, ela pode ser armazenada em um `@Field`. + +```swift +@Field(key: "pet") +var pet: Pet +``` + +Este campo pode ser armazenado usando o tipo de dado `.dictionary(of:)`. + +```swift +.field("pet", .dictionary, .required) +``` + +Como tipos `Codable` são dicionários heterogêneos, não especificamos o parâmetro `of`. + +Se os valores do dicionário fossem homogêneos, por exemplo `[String: Int]`, o parâmetro `of` especificaria o tipo de valor. + +```swift +.field("numbers", .dictionary(of: .int), .required) +``` + +Chaves de dicionário devem sempre ser strings. + +## Array + +O tipo de dado array é capaz de armazenar arrays aninhados. Isso inclui arrays Swift que contêm valores `Codable` e tipos `Codable` que usam um container sem chave. + +Considere o seguinte `@Field` que armazena um array de strings. + +```swift +@Field(key: "tags") +var tags: [String] +``` + +Este campo pode ser armazenado usando o tipo de dado `.array(of:)`. + +```swift +.field("tags", .array(of: .string), .required) +``` + +Como o array é homogêneo, especificamos o parâmetro `of`. + +`Array`s Codable do Swift sempre terão um tipo de valor homogêneo. Tipos `Codable` personalizados que serializam valores heterogêneos para containers sem chave são a exceção e devem usar o tipo de dado `.array`. + +## Enum + +O tipo de dado enum é capaz de armazenar enums Swift baseados em string nativamente. Enums nativos do banco de dados fornecem uma camada adicional de segurança de tipos ao seu banco de dados e podem ser mais performáticos que enums brutos. + +Para definir um enum nativo do banco de dados, use o método `enum` no `Database`. Use `case` para definir cada caso do enum. + +```swift +// Um exemplo de criação de enum. +database.enum("planet_type") + .case("smallRocky") + .case("gasGiant") + .case("dwarf") + .create() +``` + +Uma vez que um enum foi criado, você pode usar o método `read()` para gerar um tipo de dado para o campo do seu schema. + +```swift +// Um exemplo de leitura de um enum e uso para definir um novo campo. +database.enum("planet_type").read().flatMap { planetType in + database.schema("planets") + .field("type", planetType, .required) + .update() +} + +// Ou + +let planetType = try await database.enum("planet_type").read() +try await database.schema("planets") + .field("type", planetType, .required) + .update() +``` + +Para atualizar um enum, chame `update()`. Cases podem ser deletados de enums existentes. + +```swift +// Um exemplo de atualização de enum. +database.enum("planet_type") + .deleteCase("gasGiant") + .update() +``` + +Para deletar um enum, chame `delete()`. + +```swift +// Um exemplo de exclusão de enum. +database.enum("planet_type").delete() +``` + +## Acoplamento de Model + +A construção de schema é propositalmente desacoplada dos models. Diferentemente da construção de queries, a construção de schema não faz uso de key paths e é completamente baseada em strings. Isso é importante pois definições de schema, especialmente aquelas escritas para migrations, podem precisar referenciar propriedades de models que já não existem mais. + +Para entender melhor isso, veja o seguinte exemplo de migration. + +```swift +struct UserMigration: AsyncMigration { + func prepare(on database: Database) async throws { + try await database.schema("users") + .field("id", .uuid, .identifier(auto: false)) + .field("name", .string, .required) + .create() + } + + func revert(on database: Database) async throws { + try await database.schema("users").delete() + } +} +``` + +Vamos assumir que esta migration já foi enviada para produção. Agora vamos assumir que precisamos fazer a seguinte alteração no model User. + +```diff +- @Field(key: "name") +- var name: String ++ @Field(key: "first_name") ++ var firstName: String ++ ++ @Field(key: "last_name") ++ var lastName: String +``` + +Podemos fazer os ajustes necessários no schema do banco de dados com a seguinte migration. + +```swift +struct UserNameMigration: AsyncMigration { + func prepare(on database: Database) async throws { + try await database.schema("users") + .field("first_name", .string, .required) + .field("last_name", .string, .required) + .update() + + // Atualmente não é possível expressar esta atualização sem usar SQL personalizado. + // Isso também não tenta lidar com a divisão do nome em primeiro e último, + // pois isso requer sintaxe específica do banco de dados. + try await User.query(on: database) + .set(["first_name": .sql(embed: "name")]) + .run() + + try await database.schema("users") + .deleteField("name") + .update() + } + + func revert(on database: Database) async throws { + try await database.schema("users") + .field("name", .string, .required) + .update() + try await User.query(on: database) + .set(["name": .sql(embed: "concat(first_name, ' ', last_name)")]) + .run() + try await database.schema("users") + .deleteField("first_name") + .deleteField("last_name") + .update() + } +} +``` + +Note que para esta migration funcionar, precisamos ser capazes de referenciar tanto o campo removido `name` quanto os novos campos `firstName` e `lastName` ao mesmo tempo. Além disso, a `UserMigration` original deve continuar sendo válida. Isso não seria possível de fazer com key paths. + +## Definindo o Espaço do Model + +Para definir o [espaço para um model](model.md#database-space), passe o espaço para `schema(_:space:)` ao criar a tabela. Ex: + +```swift +try await db.schema("planets", space: "mirror_universe") + .id() + // ... + .create() +``` diff --git a/docs/fluent/transaction.pt.md b/docs/fluent/transaction.pt.md new file mode 100644 index 000000000..8d62a59be --- /dev/null +++ b/docs/fluent/transaction.pt.md @@ -0,0 +1,45 @@ +# Transações + +Transações permitem que você garanta que múltiplas operações sejam concluídas com sucesso antes de salvar dados no seu banco de dados. +Uma vez que uma transação é iniciada, você pode executar queries do Fluent normalmente. No entanto, nenhum dado será salvo no banco de dados até que a transação seja concluída. +Se um erro for lançado em qualquer ponto durante a transação (por você ou pelo banco de dados), nenhuma das alterações terá efeito. + +Para realizar uma transação, você precisa de acesso a algo que possa se conectar ao banco de dados. Isso geralmente é uma requisição HTTP recebida. Para isso, use `req.db.transaction(_ :)`: +```swift +req.db.transaction { database in + // use o database +} +``` +Uma vez dentro da closure da transação, você deve usar o banco de dados fornecido no parâmetro da closure (chamado `database` no exemplo) para realizar queries. + +Uma vez que esta closure retorne com sucesso, a transação será commitada. +```swift +var sun: Star = ... +var sirius: Star = ... + +return req.db.transaction { database in + return sun.save(on: database).flatMap { _ in + return sirius.save(on: database) + } +} +``` +O exemplo acima salvará `sun` e *então* `sirius` antes de completar a transação. Se qualquer uma das estrelas falhar ao salvar, nenhuma será salva. + +Uma vez que a transação for concluída, o resultado pode ser transformado em um future diferente, por exemplo em um status HTTP para indicar conclusão como mostrado abaixo: +```swift +return req.db.transaction { database in + // use o database e realize a transação +}.transform(to: HTTPStatus.ok) +``` + +## `async`/`await` + +Se estiver usando `async`/`await`, você pode refatorar o código para o seguinte: + +```swift +try await req.db.transaction { transaction in + try await sun.save(on: transaction) + try await sirius.save(on: transaction) +} +return .ok +``` diff --git a/docs/getting-started/folder-structure.pt.md b/docs/getting-started/folder-structure.pt.md new file mode 100644 index 000000000..317a5f877 --- /dev/null +++ b/docs/getting-started/folder-structure.pt.md @@ -0,0 +1,82 @@ +# Estrutura de Pastas + +Agora que você criou, compilou e executou sua primeira aplicação Vapor, vamos dedicar um momento para nos familiarizar com a estrutura de pastas do Vapor. A estrutura é baseada na estrutura de pastas do [SPM](spm.md), então se você já trabalhou com SPM antes, deve ser familiar. + +``` +. +├── Public +├── Sources +│ ├── App +│ │ ├── Controllers +│ │ ├── Migrations +│ │ ├── Models +│ │ ├── configure.swift +│ │ ├── entrypoint.swift +│ │ └── routes.swift +│ +├── Tests +│ └── AppTests +└── Package.swift +``` + +As seções abaixo explicam cada parte da estrutura de pastas em mais detalhes. + +## Public + +Esta pasta contém quaisquer arquivos públicos que serão servidos pela sua aplicação se o `FileMiddleware` estiver habilitado. Geralmente são imagens, folhas de estilo e scripts de navegador. Por exemplo, uma requisição para `localhost:8080/favicon.ico` verificará se `Public/favicon.ico` existe e o retornará. + +Você precisará habilitar o `FileMiddleware` no seu arquivo `configure.swift` antes que o Vapor possa servir arquivos públicos. + +```swift +// Serve arquivos do diretório `Public/` +let fileMiddleware = FileMiddleware( + publicDirectory: app.directory.publicDirectory +) +app.middleware.use(fileMiddleware) +``` + +## Sources + +Esta pasta contém todos os arquivos fonte Swift do seu projeto. +A pasta de nível superior, `App`, reflete o módulo do seu pacote, +conforme declarado no manifesto do [SwiftPM](spm.md). + +### App + +É aqui que toda a lógica da sua aplicação fica. + +#### Controllers + +Controllers são uma ótima maneira de agrupar a lógica da aplicação. A maioria dos controllers tem muitas funções que aceitam uma requisição e retornam algum tipo de resposta. + +#### Migrations + +A pasta migrations é onde ficam as migrações do seu banco de dados se você estiver usando o Fluent. + +#### Models + +A pasta models é um ótimo lugar para armazenar suas structs `Content` ou `Model`s do Fluent. + +#### configure.swift + +Este arquivo contém a função `configure(_:)`. Esse método é chamado pelo `entrypoint.swift` para configurar a `Application` recém-criada. É aqui que você deve registrar serviços como rotas, bancos de dados, providers e mais. + +#### entrypoint.swift + +Este arquivo contém o ponto de entrada `@main` da aplicação que configura e executa sua aplicação Vapor. + +#### routes.swift + +Este arquivo contém a função `routes(_:)`. Esse método é chamado perto do final de `configure(_:)` para registrar rotas na sua `Application`. + +## Tests + +Cada módulo não executável na sua pasta `Sources` pode ter uma pasta correspondente em `Tests`. Esta contém código construído sobre o módulo `XCTest` para testar seu pacote. Os testes podem ser executados usando `swift test` na linha de comando ou pressionando ⌘+U no Xcode. + +### AppTests + +Esta pasta contém os testes unitários para o código no seu módulo `App`. + +## Package.swift + +Por fim, temos o manifesto de pacote do [SPM](spm.md). diff --git a/docs/getting-started/hello-world.pt.md b/docs/getting-started/hello-world.pt.md new file mode 100644 index 000000000..ae78c0d7f --- /dev/null +++ b/docs/getting-started/hello-world.pt.md @@ -0,0 +1,87 @@ +# Olá, Mundo + +Este guia vai te levar passo a passo pela criação de um novo projeto Vapor, compilação e execução do servidor. + +Se você ainda não instalou o Swift ou o Vapor Toolbox, confira a seção de instalação. + +- [Instalação → macOS](../install/macos.md) +- [Instalação → Linux](../install/linux.md) + +!!! tip + O template usado pelo Vapor Toolbox requer Swift 6.0 ou superior + +## Novo Projeto + +O primeiro passo é criar um novo projeto Vapor no seu computador. Abra o terminal e use o comando de novo projeto do Toolbox. Isso criará uma nova pasta no diretório atual contendo o projeto. + +```sh +vapor new hello -n +``` + +!!! tip + A flag `-n` fornece um template básico respondendo automaticamente não para todas as perguntas. + +!!! tip + Você também pode obter o template mais recente do GitHub sem o Vapor Toolbox clonando o [repositório do template](https://github.com/vapor/template-bare) + +!!! tip + O Vapor e o template agora usam `async`/`await` por padrão. + Se você não pode atualizar para o macOS 12 e/ou precisa continuar usando `EventLoopFuture`s, + use a flag `--branch macos10-15`. + +Quando o comando finalizar, entre na pasta recém-criada: + +```sh +cd hello +``` + +## Compilar & Executar + +### Xcode + +Primeiro, abra o projeto no Xcode: + +```sh +open Package.swift +``` + +Ele começará automaticamente a baixar as dependências do Swift Package Manager. Isso pode demorar um pouco na primeira vez que você abre um projeto. Quando a resolução de dependências estiver completa, o Xcode vai popular os schemes disponíveis. + +No topo da janela, à direita dos botões Play e Stop, clique no nome do seu projeto para selecionar o Scheme do projeto e selecione um destino de execução apropriado — provavelmente, "My Mac". Clique no botão play para compilar e executar seu projeto. + +Você deverá ver o Console aparecer na parte inferior da janela do Xcode. + +```sh +[ INFO ] Server starting on http://127.0.0.1:8080 +``` + +### Linux + +No Linux e outros sistemas operacionais (e até no macOS se você não quiser usar o Xcode), você pode editar o projeto no seu editor de código favorito, como Vim ou VSCode. Veja os [Swift Server Guides](https://github.com/swift-server/guides/blob/main/docs/setup-and-ide-alternatives.md) para detalhes atualizados sobre como configurar outras IDEs. + +!!! tip + Se você está usando o VSCode como editor de código, recomendamos instalar a extensão oficial do Vapor: [Vapor for VS Code](https://marketplace.visualstudio.com/items?itemName=Vapor.vapor-vscode). + +Para compilar e executar seu projeto, no Terminal execute: + +```sh +swift run +``` + +Isso vai compilar e executar o projeto. A primeira vez que você executar, pode demorar um pouco para buscar e resolver as dependências. Quando estiver rodando, você deverá ver o seguinte no seu console: + +```sh +[ INFO ] Server starting on http://127.0.0.1:8080 +``` + +## Acessar o Localhost + +Abra seu navegador web e acesse localhost:8080/hello ou http://127.0.0.1:8080 + +Você deverá ver a seguinte página. + +```html +Olá, mundo! +``` + +Parabéns por criar, compilar e executar sua primeira aplicação Vapor! 🎉 diff --git a/docs/getting-started/spm.pt.md b/docs/getting-started/spm.pt.md new file mode 100644 index 000000000..5533345f3 --- /dev/null +++ b/docs/getting-started/spm.pt.md @@ -0,0 +1,93 @@ +# Swift Package Manager + +O [Swift Package Manager](https://swift.org/package-manager/) (SPM) é usado para compilar o código fonte e as dependências do seu projeto. Como o Vapor depende bastante do SPM, é uma boa ideia entender o básico de como ele funciona. + +O SPM é similar ao Cocoapods, Ruby gems e NPM. Você pode usar o SPM pela linha de comando com comandos como `swift build` e `swift test` ou com IDEs compatíveis. No entanto, diferente de alguns outros gerenciadores de pacotes, não existe um índice central de pacotes para o SPM. O SPM utiliza URLs para repositórios Git e versiona as dependências usando [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). + +## Package Manifest + +O primeiro lugar que o SPM procura no seu projeto é o package manifest. Ele deve estar sempre localizado no diretório raiz do seu projeto e nomeado como `Package.swift`. + +Dê uma olhada neste exemplo de package manifest. + +```swift +// swift-tools-version:5.8 +import PackageDescription + +let package = Package( + name: "MyApp", + platforms: [ + .macOS(.v12) + ], + dependencies: [ + .package(url: "https://github.com/vapor/vapor.git", from: "4.76.0"), + ], + targets: [ + .executableTarget( + name: "App", + dependencies: [ + .product(name: "Vapor", package: "vapor") + ] + ), + .testTarget(name: "AppTests", dependencies: [ + .target(name: "App"), + .product(name: "XCTVapor", package: "vapor"), + ]) + ] +) +``` + +Cada parte do package manifest é explicada nas seções a seguir. + +### Versão das Ferramentas + +A primeira linha de um package manifest indica a versão das ferramentas Swift necessária. Isso especifica a versão mínima do Swift que o pacote suporta. A API de descrição de pacotes também pode mudar entre versões do Swift, então essa linha garante que o Swift saberá como interpretar seu manifest. + +### Nome do Pacote + +O primeiro argumento de `Package` é o nome do pacote. Se o pacote for público, você deve usar o último segmento da URL do repositório Git como nome. + +### Plataformas + +O array `platforms` especifica quais plataformas este pacote suporta. Ao especificar `.macOS(.v12)`, este pacote requer macOS 12 ou superior. Quando o Xcode carregar este projeto, ele automaticamente definirá a versão mínima de deployment para macOS 12 para que você possa usar todas as APIs disponíveis. + +### Dependências + +Dependências são outros pacotes SPM dos quais seu pacote depende. Todas as aplicações Vapor dependem do pacote Vapor, mas você pode adicionar quantas outras dependências quiser. + +No exemplo acima, você pode ver que [vapor/vapor](https://github.com/vapor/vapor) versão 4.76.0 ou superior é uma dependência deste pacote. Quando você adiciona uma dependência ao seu pacote, deve em seguida indicar quais [targets](#targets) dependem dos módulos recém-disponíveis. + +### Targets + +Targets são todos os módulos, executáveis e testes que seu pacote contém. A maioria das aplicações Vapor terá dois targets, embora você possa adicionar quantos quiser para organizar seu código. Cada target declara de quais módulos ele depende. Você deve adicionar os nomes dos módulos aqui para poder importá-los no seu código. Um target pode depender de outros targets no seu projeto ou de quaisquer módulos expostos por pacotes que você adicionou ao array principal de [dependências](#dependências). + +## Estrutura de Pastas + +Abaixo está a estrutura de pastas típica para um pacote SPM. + +``` +. +├── Sources +│ └── App +│ └── (Código fonte) +├── Tests +│ └── AppTests +└── Package.swift +``` + +Cada `.target` ou `.executableTarget` corresponde a uma pasta na pasta `Sources`. +Cada `.testTarget` corresponde a uma pasta na pasta `Tests`. + +## Package.resolved + +Na primeira vez que você compilar seu projeto, o SPM criará um arquivo `Package.resolved` que armazena a versão de cada dependência. Na próxima vez que você compilar seu projeto, essas mesmas versões serão usadas mesmo se versões mais recentes estiverem disponíveis. + +Para atualizar suas dependências, execute `swift package update`. + +## Xcode + +Se você está usando o Xcode 11 ou superior, alterações em dependências, targets, products, etc. acontecerão automaticamente sempre que o arquivo `Package.swift` for modificado. + +Se você quiser atualizar para as dependências mais recentes, use File → Swift Packages → Update To Latest Swift Package Versions. + +Você também pode querer adicionar o arquivo `.swiftpm` ao seu `.gitignore`. É onde o Xcode armazenará a configuração do seu projeto Xcode. diff --git a/docs/getting-started/xcode.pt.md b/docs/getting-started/xcode.pt.md new file mode 100644 index 000000000..1933acdca --- /dev/null +++ b/docs/getting-started/xcode.pt.md @@ -0,0 +1,40 @@ +# Xcode + +Esta página apresenta algumas dicas e truques para usar o Xcode. Se você usa um ambiente de desenvolvimento diferente, pode pular esta seção. + +## Diretório de Trabalho Personalizado + +Por padrão, o Xcode executará seu projeto a partir da pasta _DerivedData_. Essa pasta não é a mesma que a pasta raiz do seu projeto (onde o arquivo _Package.swift_ está). Isso significa que o Vapor não conseguirá encontrar arquivos e pastas como _.env_ ou _Public_. + +Você pode perceber isso se vir o seguinte aviso ao executar sua aplicação. + +```fish +[ WARNING ] No custom working directory set for this scheme, using /path/to/DerivedData/project-abcdef/Build/ +``` + +Para corrigir isso, defina um diretório de trabalho personalizado no scheme do Xcode para seu projeto. + +Primeiro, edite o scheme do seu projeto clicando no seletor de scheme ao lado dos botões play e stop. + +![Área de Scheme do Xcode](../images/xcode-scheme-area.png) + +Selecione _Edit Scheme..._ no menu suspenso. + +![Menu de Scheme do Xcode](../images/xcode-scheme-menu.png) + +No editor de scheme, escolha a ação _App_ e a aba _Options_. Marque _Use custom working directory_ e insira o caminho para a pasta raiz do seu projeto. + +![Opções de Scheme do Xcode](../images/xcode-scheme-options.png) + +Você pode obter o caminho completo da pasta raiz do seu projeto executando `pwd` em uma janela do terminal aberta nela. + +```sh +# obter o caminho desta pasta +pwd +``` + +Você deverá ver uma saída similar à seguinte. + +``` +/caminho/do/projeto +``` diff --git a/docs/index.pt.md b/docs/index.pt.md new file mode 100644 index 000000000..b1537ef2d --- /dev/null +++ b/docs/index.pt.md @@ -0,0 +1,28 @@ +Bem-vindo à Documentação do Vapor! Vapor é um framework web para Swift, permitindo que você escreva backends, APIs de aplicações web e servidores HTTP em Swift. O Vapor é escrito em Swift, que é uma linguagem moderna, poderosa e segura, oferecendo diversos benefícios em relação às linguagens de servidor mais tradicionais. + +## Primeiros Passos + +Se esta é a sua primeira vez usando o Vapor, vá até [Instalação → macOS](install/macos.md) para instalar o Swift e o Vapor. + +Depois de instalar o Vapor, confira [Primeiros Passos → Olá, mundo](getting-started/hello-world.md) para criar sua primeira aplicação Vapor! + +## Outras Fontes + +Aqui estão outros ótimos lugares para encontrar informações sobre o Vapor. + +| nome | descrição | link | +|--------------------|---------------------------------------------------------------------------|-------------------------------------------------------------------| +| Discord do Vapor | Converse com milhares de desenvolvedores Vapor. | [acessar →](https://vapor.team) | +| Documentação da API| Documentação gerada automaticamente a partir dos comentários no código. | [acessar →](https://api.vapor.codes) | +| Stack Overflow | Faça e responda perguntas com a tag `vapor`. | [acessar →](https://stackoverflow.com/questions/tagged/vapor)| +| Fóruns do Swift | Publique na seção do Vapor nos fóruns do Swift.org. | [acessar →](https://forums.swift.org/c/related-projects/vapor)| +| Código Fonte | Aprenda como o Vapor funciona por baixo dos panos. | [acessar →](https://github.com/vapor/vapor) | +| GitHub Issues | Reporte bugs ou solicite funcionalidades no GitHub. | [acessar →](https://github.com/vapor/vapor/issues) | + +## Documentação Antiga + +A documentação de versões descontinuadas do Vapor que já chegaram ao fim de vida útil pode ser encontrada em [https://legacy.docs.vapor.codes/](https://legacy.docs.vapor.codes/). + +## Autores + +O Vapor Core Team e as centenas de membros da comunidade Vapor. diff --git a/docs/install/linux.pt.md b/docs/install/linux.pt.md new file mode 100644 index 000000000..b0e3cbbd6 --- /dev/null +++ b/docs/install/linux.pt.md @@ -0,0 +1,93 @@ +# Instalar no Linux + +Para usar o Vapor, você precisará do Swift 5.9 ou superior. Ele pode ser instalado usando a ferramenta CLI [Swiftly](https://swiftlang.github.io/swiftly/) fornecida pelo Swift Server Workgroup (recomendado), ou os toolchains disponíveis em [Swift.org](https://swift.org/download/). + +## Distribuições e Versões Suportadas + +O Vapor suporta as mesmas versões de distribuições Linux que o Swift 5.9 ou versões mais recentes suportam. Consulte a [página oficial de suporte](https://www.swift.org/platform-support/) para encontrar informações atualizadas sobre quais sistemas operacionais são oficialmente suportados. + +Distribuições Linux não oficialmente suportadas também podem executar o Swift compilando o código fonte, mas o Vapor não pode garantir estabilidade. Saiba mais sobre compilar o Swift a partir do [repositório do Swift](https://github.com/apple/swift#getting-started). + +## Instalar o Swift + +### Instalação automatizada usando a ferramenta CLI Swiftly (recomendado) + +Visite o [site do Swiftly](https://swiftlang.github.io/swiftly/) para instruções sobre como instalar o Swiftly e o Swift no Linux. Após isso, instale o Swift com o seguinte comando: + +#### Uso básico + +```sh +$ swiftly install latest + +Fetching the latest stable Swift release... +Installing Swift 5.9.1 +Downloaded 488.5 MiB of 488.5 MiB +Extracting toolchain... +Swift 5.9.1 installed successfully! + +$ swift --version + +Swift version 5.9.1 (swift-5.9.1-RELEASE) +Target: x86_64-unknown-linux-gnu +``` + +### Instalação manual com o toolchain + +Visite o guia [Using Downloads](https://swift.org/download/#using-downloads) do Swift.org para instruções sobre como instalar o Swift no Linux. + +### Fedora + +Usuários do Fedora podem simplesmente usar o seguinte comando para instalar o Swift: + +```sh +sudo dnf install swift-lang +``` + +Se você está usando o Fedora 35, precisará adicionar o EPEL 8 para obter o Swift 5.9 ou versões mais recentes. + +## Docker + +Você também pode usar as imagens Docker oficiais do Swift que já vêm com o compilador pré-instalado. Saiba mais no [Docker Hub do Swift](https://hub.docker.com/_/swift). + +## Instalar o Toolbox + +Agora que você tem o Swift instalado, vamos instalar o [Vapor Toolbox](https://github.com/vapor/toolbox). Essa ferramenta de linha de comando não é obrigatória para usar o Vapor, mas ajuda a criar novos projetos Vapor. + +### Homebrew + +O Toolbox é distribuído via Homebrew. Se você ainda não tem o Homebrew, visite brew.sh para instruções de instalação. + +```sh +brew install vapor +``` + +Verifique se a instalação foi bem-sucedida imprimindo a ajuda. + +```sh +vapor --help +``` + +Você deverá ver uma lista de comandos disponíveis. + +### Makefile + +Se preferir, você também pode compilar o Toolbox a partir do código fonte. Veja os releases do Toolbox no GitHub para encontrar a versão mais recente. + +```sh +git clone https://github.com/vapor/toolbox.git +cd toolbox +git checkout +make install +``` + +Verifique se a instalação foi bem-sucedida imprimindo a ajuda. + +```sh +vapor --help +``` + +Você deverá ver uma lista de comandos disponíveis. + +## Próximo + +Agora que você instalou o Swift e o Vapor Toolbox, crie sua primeira aplicação em [Primeiros Passos → Olá, mundo](../getting-started/hello-world.md). diff --git a/docs/install/macos.pt.md b/docs/install/macos.pt.md new file mode 100644 index 000000000..a612613a7 --- /dev/null +++ b/docs/install/macos.pt.md @@ -0,0 +1,69 @@ +# Instalar no macOS + +Para usar o Vapor no macOS, você precisará do Swift 5.9 ou superior. O Swift e todas as suas dependências vêm incluídos com o Xcode. + +## Instalar o Xcode + +Instale o [Xcode](https://itunes.apple.com/us/app/xcode/id497799835?mt=12) pela Mac App Store. + +![Xcode na Mac App Store](../images/xcode-mac-app-store.png) + +Após o download do Xcode, você deve abri-lo para concluir a instalação. Isso pode demorar um pouco. + +Verifique se a instalação foi bem-sucedida abrindo o Terminal e imprimindo a versão do Swift. + +```sh +swift --version +``` + +Você deverá ver as informações da versão do Swift. + +```sh +swift-driver version: 1.75.2 Apple Swift version 5.8 (swiftlang-5.8.0.124.2 clang-1403.0.22.11.100) +Target: arm64-apple-macosx13.0 +``` + +O Vapor 4 requer Swift 5.9 ou superior. + +## Instalar o Toolbox + +Agora que você tem o Swift instalado, vamos instalar o [Vapor Toolbox](https://github.com/vapor/toolbox). Essa ferramenta de linha de comando não é obrigatória para usar o Vapor, mas ajuda a criar novos projetos Vapor. + +### Homebrew + +O Toolbox é distribuído via Homebrew. Se você ainda não tem o Homebrew, visite brew.sh para instruções de instalação. + +```sh +brew install vapor +``` + +Verifique se a instalação foi bem-sucedida imprimindo a ajuda. + +```sh +vapor --help +``` + +Você deverá ver uma lista de comandos disponíveis. + +### Makefile + +Se preferir, você também pode compilar o Toolbox a partir do código fonte. Veja os releases do Toolbox no GitHub para encontrar a versão mais recente. + +```sh +git clone https://github.com/vapor/toolbox.git +cd toolbox +git checkout +make install +``` + +Verifique se a instalação foi bem-sucedida imprimindo a ajuda. + +```sh +vapor --help +``` + +Você deverá ver uma lista de comandos disponíveis. + +## Próximo + +Agora que você instalou o Swift e o Vapor Toolbox, crie sua primeira aplicação em [Primeiros Passos → Olá, mundo](../getting-started/hello-world.md). diff --git a/docs/leaf/custom-tags.pt.md b/docs/leaf/custom-tags.pt.md new file mode 100644 index 000000000..bd4cc4a23 --- /dev/null +++ b/docs/leaf/custom-tags.pt.md @@ -0,0 +1,128 @@ +# Tags Personalizadas + +Você pode criar tags Leaf personalizadas usando o protocolo [`LeafTag`](https://api.vapor.codes/leafkit/documentation/leafkit/leaftag). + +Para demonstrar isso, vamos criar uma tag personalizada `#now` que imprime o timestamp atual. A tag também suportará um único parâmetro opcional para especificar o formato da data. + +!!! tip "Dica" + Se a sua tag personalizada renderiza HTML, você deve conformar sua tag com `UnsafeUnescapedLeafTag` para que o HTML não seja escapado. Lembre-se de verificar ou sanitizar qualquer entrada do usuário. + +## `LeafTag` + +Primeiro, crie uma classe chamada `NowTag` e conforme-a com `LeafTag`. + +```swift +struct NowTag: LeafTag { + func render(_ ctx: LeafContext) throws -> LeafData { + ... + } +} +``` + +Agora vamos implementar o método `render(_:)`. O contexto `LeafContext` passado para este método tem tudo o que precisamos. + +```swift +enum NowTagError: Error { + case invalidFormatParameter + case tooManyParameters +} + +struct NowTag: LeafTag { + func render(_ ctx: LeafContext) throws -> LeafData { + let formatter = DateFormatter() + switch ctx.parameters.count { + case 0: formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + case 1: + guard let string = ctx.parameters[0].string else { + throw NowTagError.invalidFormatParameter + } + + formatter.dateFormat = string + default: + throw NowTagError.tooManyParameters + } + + let dateAsString = formatter.string(from: Date()) + return LeafData.string(dateAsString) + } +} +``` + +## Configurar Tag + +Agora que implementamos `NowTag`, só precisamos informar o Leaf sobre ela. Você pode adicionar qualquer tag assim — mesmo que venham de um pacote separado. Você normalmente faz isso em `configure.swift`: + +```swift +app.leaf.tags["now"] = NowTag() +``` + +E é isso! Agora podemos usar nossa tag personalizada no Leaf. + +```leaf +A hora é #now() +``` + +## Propriedades do Contexto + +O `LeafContext` contém duas propriedades importantes: `parameters` e `data`, que têm tudo o que precisamos. + +- `parameters`: Um array que contém os parâmetros da tag. +- `data`: Um dicionário que contém os dados da view passados para `render(_:_:)` como contexto. + +### Exemplo de Tag Hello + +Para ver como usar isso, vamos implementar uma tag hello simples usando ambas as propriedades. + +#### Usando Parâmetros + +Podemos acessar o primeiro parâmetro que conteria o nome. + +```swift +enum HelloTagError: Error { + case missingNameParameter +} + +struct HelloTag: UnsafeUnescapedLeafTag { + func render(_ ctx: LeafContext) throws -> LeafData { + guard let name = ctx.parameters[0].string else { + throw HelloTagError.missingNameParameter + } + + return LeafData.string("

Olá \(name)

") + } +} +``` + +```leaf +#hello("John") +``` + +#### Usando Data + +Podemos acessar o valor do nome usando a chave "name" dentro da propriedade data. + +```swift +enum HelloTagError: Error { + case nameNotFound +} + +struct HelloTag: UnsafeUnescapedLeafTag { + func render(_ ctx: LeafContext) throws -> LeafData { + guard let name = ctx.data["name"]?.string else { + throw HelloTagError.nameNotFound + } + + return LeafData.string("

Olá \(name)

") + } +} +``` + +```leaf +#hello() +``` + +_Controlador_: + +```swift +return try await req.view.render("home", ["name": "John"]) +``` diff --git a/docs/leaf/getting-started.pt.md b/docs/leaf/getting-started.pt.md new file mode 100644 index 000000000..20d0be77d --- /dev/null +++ b/docs/leaf/getting-started.pt.md @@ -0,0 +1,106 @@ +# Leaf + +Leaf é uma linguagem de templates poderosa com sintaxe inspirada em Swift. Você pode usá-la para gerar páginas HTML dinâmicas para um site front-end ou gerar e-mails ricos para enviar a partir de uma API. + +## Pacote + +O primeiro passo para usar o Leaf é adicioná-lo como dependência ao seu projeto no arquivo de manifesto do pacote SPM. + +```swift +// swift-tools-version:5.8 +import PackageDescription + +let package = Package( + name: "MyApp", + platforms: [ + .macOS(.v10_15) + ], + dependencies: [ + /// Quaisquer outras dependências ... + .package(url: "https://github.com/vapor/leaf.git", from: "4.4.0"), + ], + targets: [ + .target(name: "App", dependencies: [ + .product(name: "Leaf", package: "leaf"), + // Quaisquer outras dependências + ]), + // Outros targets + ] +) +``` + +## Configuração + +Depois de adicionar o pacote ao seu projeto, você pode configurar o Vapor para usá-lo. Isso geralmente é feito em [`configure.swift`](../getting-started/folder-structure.md#configureswift). + +```swift +import Leaf + +app.views.use(.leaf) +``` + +Isso diz ao Vapor para usar o `LeafRenderer` quando você chamar `req.view` no seu código. + +!!! warning "Aviso" + Para que o Leaf consiga encontrar os templates ao executar a partir do Xcode, você deve configurar o [diretório de trabalho personalizado](../getting-started/xcode.md#custom-working-directory) no seu workspace do Xcode. + +### Cache para Renderização de Páginas + +O Leaf possui um cache interno para renderização de páginas. Quando o ambiente da `Application` está definido como `.development`, esse cache é desabilitado, para que as alterações nos templates tenham efeito imediatamente. Em `.production` e todos os outros ambientes, o cache é habilitado por padrão. Quaisquer alterações feitas nos templates não terão efeito até que a aplicação seja reiniciada. + +Para desabilitar o cache do Leaf, faça o seguinte: + +```swift +app.leaf.cache.isEnabled = false +``` + +!!! warning "Aviso" + Embora desabilitar o cache seja útil para depuração, não é recomendado para ambientes de produção, pois pode impactar significativamente o desempenho devido à necessidade de recompilar os templates a cada requisição. + +## Estrutura de Pastas + +Depois de configurar o Leaf, você precisará garantir que tenha uma pasta `Views` para armazenar seus arquivos `.leaf`. Por padrão, o Leaf espera que a pasta de views esteja em `./Resources/Views` relativo à raiz do seu projeto. + +Você provavelmente também vai querer habilitar o [`FileMiddleware`](https://api.vapor.codes/vapor/documentation/vapor/filemiddleware) do Vapor para servir arquivos da sua pasta `/Public`, caso planeje servir arquivos Javascript e CSS, por exemplo. + +``` +VaporApp +├── Package.swift +├── Resources +│ ├── Views +│ │ └── hello.leaf +├── Public +│ ├── images (recursos de imagens) +│ ├── styles (recursos css) +└── Sources + └── ... +``` + +## Renderizando uma View + +Agora que o Leaf está configurado, vamos renderizar seu primeiro template. Dentro da pasta `Resources/Views`, crie um novo arquivo chamado `hello.leaf` com o seguinte conteúdo: + +```leaf +Olá, #(name)! +``` + +!!! tip "Dica" + Se você estiver usando o VSCode como editor de código, recomendamos instalar a extensão do Vapor para habilitar o destaque de sintaxe: [Vapor for VS Code](https://marketplace.visualstudio.com/items?itemName=Vapor.vapor-vscode). + +Então, registre uma rota (geralmente feito em `routes.swift` ou em um controller) para renderizar a view. + +```swift +app.get("hello") { req -> EventLoopFuture in + return req.view.render("hello", ["name": "Leaf"]) +} + +// ou + +app.get("hello") { req async throws -> View in + return try await req.view.render("hello", ["name": "Leaf"]) +} +``` + +Isso usa a propriedade genérica `view` no `Request` em vez de chamar o Leaf diretamente. Isso permite que você troque para um renderizador diferente nos seus testes. + +Abra seu navegador e acesse `/hello`. Você deverá ver `Olá, Leaf!`. Parabéns por renderizar sua primeira view com Leaf! diff --git a/docs/leaf/overview.pt.md b/docs/leaf/overview.pt.md new file mode 100644 index 000000000..dc8003f27 --- /dev/null +++ b/docs/leaf/overview.pt.md @@ -0,0 +1,291 @@ +# Visão Geral do Leaf + +Leaf é uma linguagem de templates poderosa com sintaxe inspirada em Swift. Você pode usá-la para gerar páginas HTML dinâmicas para um site front-end ou gerar e-mails ricos para enviar a partir de uma API. + +Este guia fornecerá uma visão geral da sintaxe do Leaf e das tags disponíveis. + +## Sintaxe de Template + +Aqui está um exemplo de uso básico de uma tag Leaf. + +```leaf +Existem #count(users) usuários. +``` + +As tags Leaf são compostas por quatro elementos: + +- Token `#`: Sinaliza ao parser do Leaf para começar a procurar uma tag. +- Nome `count`: identifica a tag. +- Lista de Parâmetros `(users)`: Pode aceitar zero ou mais argumentos. +- Corpo: Um corpo opcional pode ser fornecido para algumas tags usando dois-pontos e uma tag de fechamento. + +Pode haver muitos usos diferentes desses quatro elementos dependendo da implementação da tag. Vamos ver alguns exemplos de como as tags integradas do Leaf podem ser usadas: + +```leaf +#(variable) +#extend("template"): Eu sou adicionado a um template base! #endextend +#export("title"): Bem-vindo ao Vapor #endexport +#import("body") +#count(friends) +#for(friend in friends):
  • #(friend.name)
  • #endfor +``` + +O Leaf também suporta muitas expressões familiares do Swift. + +- `+` +- `%` +- `>` +- `==` +- `||` +- etc. + +```leaf +#if(1 + 1 == 2): + Olá! +#endif + +#if(index % 2 == 0): + Este é um índice par. +#else: + Este é um índice ímpar. +#endif +``` + +## Contexto + +No exemplo de [Primeiros Passos](getting-started.md), usamos um dicionário `[String: String]` para passar dados ao Leaf. No entanto, você pode passar qualquer coisa que conforme com `Encodable`. Na verdade, é preferível usar structs `Encodable`, já que `[String: Any]` não é suportado. Isso significa que você *não pode* passar um array diretamente, e deve envolvê-lo em uma struct: + +```swift +struct WelcomeContext: Encodable { + var title: String + var numbers: [Int] +} +return req.view.render("home", WelcomeContext(title: "Olá!", numbers: [42, 9001])) +``` + +Isso expõe `title` e `numbers` ao nosso template Leaf, que podem então ser usados dentro de tags. Por exemplo: + +```leaf +

    #(title)

    +#for(number in numbers): +

    #(number)

    +#endfor +``` + +## Uso + +Aqui estão alguns exemplos comuns de uso do Leaf. + +### Condições + +O Leaf é capaz de avaliar uma série de condições usando sua tag `#if`. Por exemplo, se você fornecer uma variável, ele verificará se essa variável existe no contexto: + +```leaf +#if(title): + O título é #(title) +#else: + Nenhum título foi fornecido. +#endif +``` + +Você também pode escrever comparações, por exemplo: + +```leaf +#if(title == "Welcome"): + Esta é uma página web amigável. +#else: + Estranhos não são permitidos! +#endif +``` + +Se você quiser usar outra tag como parte da sua condição, deve omitir o `#` da tag interna. Por exemplo: + +```leaf +#if(count(users) > 0): + Você tem usuários! +#else: + Não há usuários ainda :( +#endif +``` + +Você também pode usar declarações `#elseif`: + +```leaf +#if(title == "Welcome"): + Olá, novo usuário! +#elseif(title == "Welcome back!"): + Olá, usuário antigo +#else: + Página inesperada! +#endif +``` + +### Loops + +Se você fornecer um array de itens, o Leaf pode iterar sobre eles e permitir que você manipule cada item individualmente usando sua tag `#for`. + +Por exemplo, poderíamos atualizar nosso código Swift para fornecer uma lista de planetas: + +```swift +struct SolarSystem: Codable { + let planets = ["Venus", "Earth", "Mars"] +} + +return req.view.render("solarSystem", SolarSystem()) +``` + +Poderíamos então iterar sobre eles no Leaf assim: + +```leaf +Planetas: +
      +#for(planet in planets): +
    • #(planet)
    • +#endfor +
    +``` + +Isso renderizaria uma view assim: + +``` +Planetas: +- Venus +- Earth +- Mars +``` + +### Estendendo Templates + +A tag `#extend` do Leaf permite que você copie o conteúdo de um template para outro. Ao usar isso, você deve sempre omitir a extensão .leaf do arquivo de template. + +Estender é útil para copiar um conteúdo padrão, por exemplo um rodapé de página, código de anúncio ou tabela compartilhada entre múltiplas páginas: + +```leaf +#extend("footer") +``` + +Esta tag também é útil para construir um template em cima de outro. Por exemplo, você pode ter um arquivo layout.leaf que inclui todo o código necessário para o layout do seu site — estrutura HTML, CSS e JavaScript — com algumas lacunas onde o conteúdo da página varia. + +Usando esta abordagem, você construiria um template filho que preenche seu conteúdo único e então estende o template pai que posiciona o conteúdo adequadamente. Para fazer isso, você pode usar as tags `#export` e `#import` para armazenar e posteriormente recuperar conteúdo do contexto. + +Por exemplo, você pode criar um template `child.leaf` assim: + +```leaf +#extend("main"): + #export("body"): +

    Bem-vindo ao Vapor!

    + #endexport +#endextend +``` + +Chamamos `#export` para armazenar algum HTML e torná-lo disponível para o template que estamos estendendo. Então renderizamos `main.leaf` e usamos os dados exportados quando necessário, junto com quaisquer outras variáveis de contexto passadas pelo Swift. Por exemplo, `main.leaf` pode ser assim: + +```leaf + + + #(title) + + #import("body") + +``` + +Aqui estamos usando `#import` para buscar o conteúdo passado para a tag `#extend`. Quando passamos `["title": "Olá!"]` pelo Swift, `child.leaf` renderizará da seguinte forma: + +```html + + + Olá! + +

    Bem-vindo ao Vapor!

    + +``` + +### Outras Tags + +#### `#count` + +A tag `#count` retorna o número de itens em um array. Por exemplo: + +```leaf +Sua busca encontrou #count(matches) páginas. +``` + +#### `#lowercased` + +A tag `#lowercased` converte todas as letras de uma string para minúsculas. + +```leaf +#lowercased(name) +``` + +#### `#uppercased` + +A tag `#uppercased` converte todas as letras de uma string para maiúsculas. + +```leaf +#uppercased(name) +``` + +#### `#capitalized` + +A tag `#capitalized` converte a primeira letra de cada palavra de uma string para maiúscula e as demais para minúsculas. Veja [`String.capitalized`](https://developer.apple.com/documentation/foundation/nsstring/1416784-capitalized) para mais informações. + +```leaf +#capitalized(name) +``` + +#### `#contains` + +A tag `#contains` aceita um array e um valor como seus dois parâmetros, e retorna verdadeiro se o array no primeiro parâmetro contém o valor do segundo parâmetro. + +```leaf +#if(contains(planets, "Earth")): + A Terra está aqui! +#else: + A Terra não está neste array. +#endif +``` + +#### `#date` + +A tag `#date` formata datas em uma string legível. Por padrão, usa a formatação ISO8601. + +```swift +render(..., ["now": Date()]) +``` + +```leaf +A hora é #date(now) +``` + +Você pode passar uma string de formato de data personalizado como segundo argumento. Veja o [`DateFormatter`](https://developer.apple.com/documentation/foundation/dateformatter) do Swift para mais informações. + +```leaf +A data é #date(now, "yyyy-MM-dd") +``` + +Você também pode passar um ID de fuso horário para o formatador de data como terceiro argumento. Veja [`DateFormatter.timeZone`](https://developer.apple.com/documentation/foundation/dateformatter/1411406-timezone) e [`TimeZone`](https://developer.apple.com/documentation/foundation/timezone) do Swift para mais informações. + +```leaf +A data é #date(now, "yyyy-MM-dd", "America/New_York") +``` + +#### `#unsafeHTML` + +A tag `#unsafeHTML` age como uma tag de variável — ex: `#(variable)`. No entanto, ela não escapa nenhum HTML que `variable` possa conter: + +```leaf +A hora é #unsafeHTML(styledTitle) +``` + +!!! note "Nota" + Você deve ter cuidado ao usar esta tag para garantir que a variável fornecida não exponha seus usuários a um ataque XSS. + +#### `#dumpContext` + +A tag `#dumpContext` renderiza todo o contexto em uma string legível. Use esta tag para depurar o que está sendo fornecido como contexto para a renderização atual. + +```leaf +Olá, mundo! +#dumpContext +``` diff --git a/docs/redis/overview.pt.md b/docs/redis/overview.pt.md new file mode 100644 index 000000000..9f61f6102 --- /dev/null +++ b/docs/redis/overview.pt.md @@ -0,0 +1,169 @@ +# Redis + +O [Redis](https://redis.io/) é um dos armazenamentos de estruturas de dados em memória mais populares, comumente usado como cache ou broker de mensagens. + +Esta biblioteca é uma integração entre o Vapor e o [**RediStack**](https://github.com/swift-server/RediStack), que é o driver subjacente que se comunica com o Redis. + +!!! note "Nota" + A maioria das funcionalidades do Redis é fornecida pelo **RediStack**. + Recomendamos fortemente que você se familiarize com sua documentação. + + _Links são fornecidos quando apropriado._ + +## Pacote + +O primeiro passo para usar o Redis é adicioná-lo como dependência ao seu projeto no manifesto do pacote Swift. + +> Este exemplo é para um pacote existente. Para ajuda sobre como iniciar um novo projeto, consulte o guia principal de [Primeiros Passos](../getting-started/hello-world.md). + +```swift +dependencies: [ + // ... + .package(url: "https://github.com/vapor/redis.git", from: "4.0.0") +] +// ... +targets: [ + .target(name: "App", dependencies: [ + // ... + .product(name: "Redis", package: "redis") + ]) +] +``` + +## Configuração + +O Vapor emprega uma estratégia de pool para instâncias de [`RedisConnection`](https://swiftpackageindex.com/swift-server/RediStack/main/documentation/redistack/redisconnection), e há várias opções para configurar conexões individuais, bem como os pools em si. + +O mínimo necessário para configurar o Redis é fornecer uma URL para conexão: + +```swift +let app = Application() + +app.redis.configuration = try RedisConfiguration(hostname: "localhost") +``` + +### Configuração do Redis + +> Documentação da API: [`RedisConfiguration`](https://api.vapor.codes/redis/documentation/redis/redisconfiguration) + +#### serverAddresses + +Se você tem múltiplos endpoints Redis, como um cluster de instâncias Redis, você vai querer criar uma coleção [`[SocketAddress]`](https://swiftpackageindex.com/apple/swift-nio/main/documentation/niocore/socketaddress) para passar no inicializador. + +A forma mais comum de criar um `SocketAddress` é com o método estático [`makeAddressResolvingHost(_:port:)`](https://swiftpackageindex.com/apple/swift-nio/main/documentation/niocore/socketaddress/makeaddressresolvinghost(_:port:)). + +```swift +let serverAddresses: [SocketAddress] = [ + try .makeAddressResolvingHost("localhost", port: RedisConnection.Configuration.defaultPort) +] +``` + +Para um único endpoint Redis, pode ser mais fácil trabalhar com os inicializadores de conveniência, pois eles cuidam da criação do `SocketAddress` para você: + +- [`.init(url:pool)`](https://api.vapor.codes/redis/documentation/redis/redisconfiguration/init(url:tlsconfiguration:pool:)-o9lf) (com `String` ou [`Foundation.URL`](https://developer.apple.com/documentation/foundation/url)) +- [`.init(hostname:port:password:database:pool:)`](https://api.vapor.codes/redis/documentation/redis/redisconfiguration/init(hostname:port:password:tlsconfiguration:database:pool:)) + +#### password + +Se sua instância Redis é protegida por senha, você precisará passá-la como argumento `password`. + +Cada conexão, ao ser criada, será autenticada usando a senha. + +#### database + +Este é o índice do banco de dados que você deseja selecionar quando cada conexão é criada. + +Isso evita que você tenha que enviar o comando `SELECT` para o Redis manualmente. + +!!! warning "Aviso" + A seleção do banco de dados não é mantida. Tenha cuidado ao enviar o comando `SELECT` por conta própria. + +### Opções do Pool de Conexões + +> Documentação da API: [`RedisConfiguration.PoolOptions`](https://api.vapor.codes/redis/documentation/redis/redisconfiguration/pooloptions) + +!!! note "Nota" + Apenas as opções mais comumente alteradas são destacadas aqui. Para todas as opções, consulte a documentação da API. + +#### minimumConnectionCount + +Este é o valor para definir quantas conexões você quer que cada pool mantenha o tempo todo. + +Se o valor for `0`, então se as conexões forem perdidas por qualquer motivo, o pool não as recriará até que sejam necessárias. + +Isso é conhecido como uma conexão de "cold start" e tem alguma sobrecarga em comparação com manter uma contagem mínima de conexões. + +#### maximumConnectionCount + +Esta opção determina o comportamento de como a contagem máxima de conexões é mantida. + +!!! seealso "Veja Também" + Consulte a API `RedisConnectionPoolSize` para se familiarizar com as opções disponíveis. + +## Enviando um Comando + +Você pode enviar comandos usando a propriedade `.redis` em qualquer instância de [`Application`](https://api.vapor.codes/vapor/documentation/vapor/application) ou [`Request`](https://api.vapor.codes/vapor/documentation/vapor/request), que lhe dará acesso a um [`RedisClient`](https://swiftpackageindex.com/swift-server/RediStack/main/documentation/redistack/redisclient). + +Qualquer `RedisClient` possui diversas extensões para todos os vários [comandos do Redis](https://redis.io/commands). + +```swift +let value = try app.redis.get("my_key", as: String.self).wait() +print(value) +// Optional("my_value") + +// ou + +let value = try await app.redis.get("my_key", as: String.self) +print(value) +// Optional("my_value") +``` + +### Comandos Não Suportados + +Caso o **RediStack** não suporte um comando com um método de extensão, você ainda pode enviá-lo manualmente. + +```swift +// cada valor após o comando é o argumento posicional que o Redis espera +try app.redis.send(command: "PING", with: ["hello"]) + .map { + print($0) + } + .wait() +// "hello" + +// ou + +let res = try await app.redis.send(command: "PING", with: ["hello"]) +print(res) +// "hello" +``` + +## Modo Pub/Sub + +O Redis suporta a capacidade de entrar em um [modo "Pub/Sub"](https://redis.io/topics/pubsub) onde uma conexão pode ouvir "canais" específicos e executar closures específicas quando os canais inscritos publicam uma "mensagem" (algum valor de dados). + +Existe um ciclo de vida definido para uma assinatura: + +1. **subscribe**: invocado uma vez quando a assinatura começa +1. **message**: invocado 0+ vezes conforme mensagens são publicadas nos canais inscritos +1. **unsubscribe**: invocado uma vez quando a assinatura termina, seja por solicitação ou pela perda da conexão + +Ao criar uma assinatura, você deve fornecer pelo menos um [`messageReceiver`](https://swiftpackageindex.com/swift-server/RediStack/main/documentation/redistack/redissubscriptionmessagereceiver) para lidar com todas as mensagens publicadas pelo canal inscrito. + +Opcionalmente, você pode fornecer um `RedisSubscriptionChangeHandler` para `onSubscribe` e `onUnsubscribe` para lidar com seus respectivos eventos do ciclo de vida. + +```swift +// cria 2 assinaturas, uma para cada canal fornecido +app.redis.subscribe + to: "channel_1", "channel_2", + messageReceiver: { channel, message in + switch channel { + case "channel_1": // fazer algo com a mensagem + default: break + } + }, + onUnsubscribe: { channel, subscriptionCount in + print("cancelou inscrição de \(channel)") + print("inscrições restantes: \(subscriptionCount)") + } +``` diff --git a/docs/redis/sessions.pt.md b/docs/redis/sessions.pt.md new file mode 100644 index 000000000..6b8cddcd6 --- /dev/null +++ b/docs/redis/sessions.pt.md @@ -0,0 +1,78 @@ +# Redis e Sessões + +O Redis pode atuar como um provedor de armazenamento para fazer cache de [dados de sessão](../advanced/sessions.md#session-data), como credenciais de usuário. + +Se um [`RedisSessionsDelegate`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate) personalizado não for fornecido, um padrão será utilizado. + +## Comportamento Padrão + +### Criação do SessionID + +A menos que você implemente o método [`makeNewID()`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate/makenewid()-3hyne) no [seu próprio `RedisSessionsDelegate`](#redissessionsdelegate), todos os valores de [`SessionID`](https://api.vapor.codes/vapor/documentation/vapor/sessionid) serão criados da seguinte forma: + +1. Gerar 32 bytes de caracteres aleatórios +1. Codificar o valor em base64 + +Por exemplo: `Hbxozx8rTj+XXGWAzOhh1npZFXaGLpTWpWCaXuo44xQ=` + +### Armazenamento de SessionData + +A implementação padrão de `RedisSessionsDelegate` armazenará [`SessionData`](https://api.vapor.codes/vapor/documentation/vapor/sessiondata) como uma string JSON simples usando `Codable`. + +A menos que você implemente o método [`makeRedisKey(for:)`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate/makerediskey(for:)-5nfge) no seu próprio `RedisSessionsDelegate`, o `SessionData` será armazenado no Redis com uma chave que prefixa o `SessionID` com `vrs-` (**V**apor **R**edis **S**essions) + +Por exemplo: `vrs-Hbxozx8rTj+XXGWAzOhh1npZFXaGLpTWpWCaXuo44xQ=` + +## Registrando um Delegate Personalizado + +Para personalizar como os dados são lidos e escritos no Redis, registre seu próprio objeto `RedisSessionsDelegate` da seguinte forma: + +```swift +import Redis + +struct CustomRedisSessionsDelegate: RedisSessionsDelegate { + // implementação +} + +app.sessions.use(.redis(delegate: CustomRedisSessionsDelegate())) +``` + +## RedisSessionsDelegate + +> Documentação da API: [`RedisSessionsDelegate`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate) + +Um objeto que conforma com este protocolo pode ser usado para alterar como o `SessionData` é armazenado no Redis. + +Apenas dois métodos são obrigatórios para um tipo que conforma com o protocolo: [`redis(_:store:with:)`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate/redis(_:store:with:)) e [`redis(_:fetchDataFor:)`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate/redis(_:fetchdatafor:)). + +Ambos são obrigatórios, pois a forma como você personaliza a escrita dos dados de sessão no Redis está intrinsecamente ligada à forma como eles são lidos. + +### Exemplo de Hash com RedisSessionsDelegate + +Por exemplo, se você quisesse armazenar os dados de sessão como um [**Hash** no Redis](https://redis.io/topics/data-types-intro#redis-hashes), você implementaria algo como o seguinte: + +```swift +func redis( + _ client: Client, + store data: SessionData, + with key: RedisKey +) -> EventLoopFuture { + // armazena cada campo de dados como um campo separado do hash + return client.hmset(data.snapshot, in: key) +} +func redis( + _ client: Client, + fetchDataFor key: RedisKey +) -> EventLoopFuture { + return client + .hgetall(from: key) + .map { hash in + // hash é [String: RESPValue] então precisamos tentar desempacotar + // o valor como string e armazenar cada valor no contêiner de dados + return hash.reduce(into: SessionData()) { result, next in + guard let value = next.value.string else { return } + result[next.key] = value + } + } +} +``` diff --git a/docs/security/authentication.pt.md b/docs/security/authentication.pt.md new file mode 100644 index 000000000..1f4f89c68 --- /dev/null +++ b/docs/security/authentication.pt.md @@ -0,0 +1,894 @@ +# Autenticação + +Autenticação é o ato de verificar a identidade de um usuário. Isso é feito através da verificação de credenciais como nome de usuário e senha ou token único. Autenticação (às vezes chamada de auth/c) é distinta de autorização (auth/z), que é o ato de verificar as permissões de um usuário previamente autenticado para realizar determinadas tarefas. + +## Introdução + +A API de Autenticação do Vapor fornece suporte para autenticar um usuário através do header `Authorization`, usando [Basic](https://tools.ietf.org/html/rfc7617) e [Bearer](https://tools.ietf.org/html/rfc6750). Também suporta autenticação de um usuário através dos dados decodificados da API de [Conteúdo](../basics/content.md). + +A autenticação é implementada criando um `Authenticator` que contém a lógica de verificação. Um authenticator pode ser usado para proteger grupos de rotas individuais ou uma aplicação inteira. Os seguintes helpers de authenticator vêm com o Vapor: + +|Protocolo|Descrição| +|-|-| +|`RequestAuthenticator`/`AsyncRequestAuthenticator`|Authenticator base capaz de criar middleware.| +|[`BasicAuthenticator`/`AsyncBasicAuthenticator`](#basic)|Autentica o header de autorização Basic.| +|[`BearerAuthenticator`/`AsyncBearerAuthenticator`](#bearer)|Autentica o header de autorização Bearer.| +|`CredentialsAuthenticator`/`AsyncCredentialsAuthenticator`|Autentica um payload de credenciais do corpo da requisição.| + +Se a autenticação for bem-sucedida, o authenticator adiciona o usuário verificado a `req.auth`. Este usuário pode então ser acessado usando `req.auth.get(_:)` em rotas protegidas pelo authenticator. Se a autenticação falhar, o usuário não é adicionado a `req.auth` e qualquer tentativa de acessá-lo falhará. + +## Authenticatable + +Para usar a API de Autenticação, você primeiro precisa de um tipo de usuário que conforme com `Authenticatable`. Isso pode ser uma `struct`, `class` ou até mesmo um `Model` do Fluent. Os exemplos a seguir assumem esta simples struct `User` que tem uma propriedade: `name`. + +```swift +import Vapor + +struct User: Authenticatable { + var name: String +} +``` + +Cada exemplo abaixo usará uma instância de um authenticator que criamos. Nestes exemplos, chamamos de `UserAuthenticator`. + +### Rota + +Authenticators são middleware e podem ser usados para proteger rotas. + +```swift +let protected = app.grouped(UserAuthenticator()) +protected.get("me") { req -> String in + try req.auth.require(User.self).name +} +``` + +`req.auth.require` é usado para buscar o `User` autenticado. Se a autenticação falhou, este método lançará um erro, protegendo a rota. + +### Guard Middleware + +Você também pode usar `GuardMiddleware` no seu grupo de rotas para garantir que um usuário foi autenticado antes de chegar ao seu route handler. + +```swift +let protected = app.grouped(UserAuthenticator()) + .grouped(User.guardMiddleware()) +``` + +Exigir autenticação não é feito pelo middleware do authenticator para permitir composição de authenticators. Leia mais sobre [composição](#composicao) abaixo. + +## Basic + +A autenticação Basic envia um nome de usuário e senha no header `Authorization`. O nome de usuário e a senha são concatenados com dois-pontos (ex: `test:secret`), codificados em base-64, e prefixados com `"Basic "`. O exemplo de requisição a seguir codifica o nome de usuário `test` com a senha `secret`. + +```http +GET /me HTTP/1.1 +Authorization: Basic dGVzdDpzZWNyZXQ= +``` + +A autenticação Basic é tipicamente usada uma vez para fazer login de um usuário e gerar um token. Isso minimiza a frequência com que a senha sensível do usuário precisa ser enviada. Você nunca deve enviar autorização Basic por uma conexão de texto puro ou TLS não verificada. + +Para implementar a autenticação Basic na sua aplicação, crie um novo authenticator conformando com `BasicAuthenticator`. Abaixo está um exemplo de authenticator com valores fixos para verificar a requisição acima. + +```swift +import Vapor + +struct UserAuthenticator: BasicAuthenticator { + typealias User = App.User + + func authenticate( + basic: BasicAuthorization, + for request: Request + ) -> EventLoopFuture { + if basic.username == "test" && basic.password == "secret" { + request.auth.login(User(name: "Vapor")) + } + return request.eventLoop.makeSucceededFuture(()) + } +} +``` + +Se você estiver usando `async`/`await`, pode usar `AsyncBasicAuthenticator`: + +```swift +import Vapor + +struct UserAuthenticator: AsyncBasicAuthenticator { + typealias User = App.User + + func authenticate( + basic: BasicAuthorization, + for request: Request + ) async throws { + if basic.username == "test" && basic.password == "secret" { + request.auth.login(User(name: "Vapor")) + } + } +} +``` + +Este protocolo requer que você implemente `authenticate(basic:for:)`, que será chamado quando uma requisição recebida contiver o header `Authorization: Basic ...`. Uma struct `BasicAuthorization` contendo o nome de usuário e a senha é passada para o método. + +Neste authenticator de teste, o nome de usuário e a senha são testados contra valores fixos. Em um authenticator real, você pode verificar contra um banco de dados ou API externa. É por isso que o método `authenticate` permite retornar um future. + +!!! tip "Dica" + Senhas nunca devem ser armazenadas em um banco de dados como texto puro. Sempre use hashes de senha para comparação. + +Se os parâmetros de autenticação estiverem corretos, neste caso correspondendo aos valores fixos, um `User` chamado Vapor é logado. Se os parâmetros de autenticação não corresponderem, nenhum usuário é logado, o que significa que a autenticação falhou. + +Se você adicionar este authenticator à sua aplicação e testar a rota definida acima, você deverá ver o nome `"Vapor"` retornado para um login bem-sucedido. Se as credenciais não estiverem corretas, você deverá ver um erro `401 Unauthorized`. + +## Bearer + +A autenticação Bearer envia um token no header `Authorization`. O token é prefixado com `"Bearer "`. O exemplo de requisição a seguir envia o token `foo`. + +```http +GET /me HTTP/1.1 +Authorization: Bearer foo +``` + +A autenticação Bearer é comumente usada para autenticação de endpoints de API. O usuário tipicamente solicita um Bearer token enviando credenciais como nome de usuário e senha para um endpoint de login. Este token pode durar minutos ou dias dependendo das necessidades da aplicação. + +Enquanto o token for válido, o usuário pode usá-lo no lugar de suas credenciais para se autenticar na API. Se o token se tornar inválido, um novo pode ser gerado usando o endpoint de login. + +Para implementar a autenticação Bearer na sua aplicação, crie um novo authenticator conformando com `BearerAuthenticator`. Abaixo está um exemplo de authenticator com valores fixos para verificar a requisição acima. + +```swift +import Vapor + +struct UserAuthenticator: BearerAuthenticator { + typealias User = App.User + + func authenticate( + bearer: BearerAuthorization, + for request: Request + ) -> EventLoopFuture { + if bearer.token == "foo" { + request.auth.login(User(name: "Vapor")) + } + return request.eventLoop.makeSucceededFuture(()) + } +} +``` + +Se você estiver usando `async`/`await`, pode usar `AsyncBearerAuthenticator`: + +```swift +import Vapor + +struct UserAuthenticator: AsyncBearerAuthenticator { + typealias User = App.User + + func authenticate( + bearer: BearerAuthorization, + for request: Request + ) async throws { + if bearer.token == "foo" { + request.auth.login(User(name: "Vapor")) + } + } +} +``` + +Este protocolo requer que você implemente `authenticate(bearer:for:)`, que será chamado quando uma requisição recebida contiver o header `Authorization: Bearer ...`. Uma struct `BearerAuthorization` contendo o token é passada para o método. + +Neste authenticator de teste, o token é testado contra um valor fixo. Em um authenticator real, você pode verificar o token consultando um banco de dados ou usando medidas criptográficas, como é feito com JWT. É por isso que o método `authenticate` permite retornar um future. + +!!! tip "Dica" + Ao implementar verificação de token, é importante considerar a escalabilidade horizontal. Se sua aplicação precisa lidar com muitos usuários simultaneamente, a autenticação pode ser um potencial gargalo. Considere como seu design irá escalar em múltiplas instâncias da sua aplicação rodando ao mesmo tempo. + +Se os parâmetros de autenticação estiverem corretos, neste caso correspondendo ao valor fixo, um `User` chamado Vapor é logado. Se os parâmetros de autenticação não corresponderem, nenhum usuário é logado, o que significa que a autenticação falhou. + +Se você adicionar este authenticator à sua aplicação e testar a rota definida acima, você deverá ver o nome `"Vapor"` retornado para um login bem-sucedido. Se as credenciais não estiverem corretas, você deverá ver um erro `401 Unauthorized`. + +## Composição + +Múltiplos authenticators podem ser compostos (combinados) para criar autenticação de endpoint mais complexa. Como um middleware de authenticator não rejeita a requisição se a autenticação falhar, mais de um desses middleware pode ser encadeado. Authenticators podem ser compostos de duas formas principais. + +### Compondo Métodos + +O primeiro método de composição de autenticação é encadear mais de um authenticator para o mesmo tipo de usuário. Veja o seguinte exemplo: + +```swift +app.grouped(UserPasswordAuthenticator()) + .grouped(UserTokenAuthenticator()) + .grouped(User.guardMiddleware()) + .post("login") +{ req in + let user = try req.auth.require(User.self) + // Fazer algo com o user. +} +``` + +Este exemplo assume dois authenticators `UserPasswordAuthenticator` e `UserTokenAuthenticator` que ambos autenticam `User`. Ambos são adicionados ao grupo de rotas. Finalmente, `GuardMiddleware` é adicionado após os authenticators para exigir que `User` foi autenticado com sucesso. + +Esta composição de authenticators resulta em uma rota que pode ser acessada por senha ou token. Tal rota poderia permitir que um usuário faça login e gere um token, e então continue usando esse token para gerar novos tokens. + +### Compondo Usuários + +O segundo método de composição de autenticação é encadear authenticators para diferentes tipos de usuário. Veja o seguinte exemplo: + +```swift +app.grouped(AdminAuthenticator()) + .grouped(UserAuthenticator()) + .get("secure") +{ req in + guard req.auth.has(Admin.self) || req.auth.has(User.self) else { + throw Abort(.unauthorized) + } + // Fazer algo. +} +``` + +Este exemplo assume dois authenticators `AdminAuthenticator` e `UserAuthenticator` que autenticam `Admin` e `User`, respectivamente. Ambos são adicionados ao grupo de rotas. Em vez de usar `GuardMiddleware`, uma verificação no route handler é adicionada para ver se `Admin` ou `User` foram autenticados. Se não, um erro é lançado. + +Esta composição de authenticators resulta em uma rota que pode ser acessada por dois tipos diferentes de usuários com métodos de autenticação potencialmente diferentes. Tal rota poderia permitir autenticação de usuário normal enquanto ainda dá acesso a um super-usuário. + +## Manual + +Você também pode lidar com autenticação manualmente usando `req.auth`. Isso é especialmente útil para testes. + +Para logar manualmente um usuário, use `req.auth.login(_:)`. Qualquer usuário `Authenticatable` pode ser passado para este método. + +```swift +req.auth.login(User(name: "Vapor")) +``` + +Para obter o usuário autenticado, use `req.auth.require(_:)` + +```swift +let user: User = try req.auth.require(User.self) +print(user.name) // String +``` + +Você também pode usar `req.auth.get(_:)` se não quiser lançar um erro automaticamente quando a autenticação falhar. + +```swift +let user = req.auth.get(User.self) +print(user?.name) // String? +``` + +Para desautenticar um usuário, passe o tipo de usuário para `req.auth.logout(_:)`. + +```swift +req.auth.logout(User.self) +``` + +## Fluent + +O [Fluent](../fluent/overview.md) define dois protocolos `ModelAuthenticatable` e `ModelTokenAuthenticatable` que podem ser adicionados aos seus modelos existentes. Conformar seus modelos a esses protocolos permite a criação de authenticators para proteger endpoints. + +`ModelTokenAuthenticatable` autentica com um Bearer token. É o que você usa para proteger a maioria dos seus endpoints. `ModelAuthenticatable` autentica com nome de usuário e senha e é usado por um único endpoint para gerar tokens. + +Este guia assume que você está familiarizado com o Fluent e configurou sua aplicação com sucesso para usar um banco de dados. Se você é novo no Fluent, comece com a [visão geral](../fluent/overview.md). + +### User + +Para começar, você precisará de um modelo representando o usuário que será autenticado. Para este guia, usaremos o seguinte modelo, mas você é livre para usar um modelo existente. + +```swift +import Fluent +import Vapor + +final class User: Model, Content { + static let schema = "users" + + @ID(key: .id) + var id: UUID? + + @Field(key: "name") + var name: String + + @Field(key: "email") + var email: String + + @Field(key: "password_hash") + var passwordHash: String + + init() { } + + init(id: UUID? = nil, name: String, email: String, passwordHash: String) { + self.id = id + self.name = name + self.email = email + self.passwordHash = passwordHash + } +} +``` + +O modelo deve ser capaz de armazenar um nome de usuário, neste caso um e-mail, e um hash de senha. Também definimos `email` como um campo único para evitar usuários duplicados. A migration correspondente para este modelo de exemplo está aqui: + +```swift +import Fluent +import Vapor + +extension User { + struct Migration: AsyncMigration { + var name: String { "CreateUser" } + + func prepare(on database: Database) async throws { + try await database.schema("users") + .id() + .field("name", .string, .required) + .field("email", .string, .required) + .field("password_hash", .string, .required) + .unique(on: "email") + .create() + } + + func revert(on database: Database) async throws { + try await database.schema("users").delete() + } + } +} +``` + +Não esqueça de adicionar a migration a `app.migrations`. + +```swift +app.migrations.add(User.Migration()) +``` + +!!! tip "Dica" + Como endereços de e-mail não são sensíveis a maiúsculas/minúsculas, você pode querer adicionar um [`Middleware`](../fluent/model.md#lifecycle) que converte o endereço de e-mail para minúsculas antes de salvá-lo no banco de dados. Esteja ciente, porém, que `ModelAuthenticatable` usa uma comparação sensível a maiúsculas/minúsculas, então se fizer isso você vai querer garantir que a entrada do usuário esteja toda em minúsculas, seja com conversão no cliente ou com um authenticator personalizado. + +A primeira coisa que você precisará é de um endpoint para criar novos usuários. Vamos usar `POST /users`. Crie uma struct [Content](../basics/content.md) representando os dados que este endpoint espera. + +```swift +import Vapor + +extension User { + struct Create: Content { + var name: String + var email: String + var password: String + var confirmPassword: String + } +} +``` + +Se quiser, você pode conformar esta struct com [Validatable](../basics/validation.md) para adicionar requisitos de validação. + +```swift +import Vapor + +extension User.Create: Validatable { + static func validations(_ validations: inout Validations) { + validations.add("name", as: String.self, is: !.empty) + validations.add("email", as: String.self, is: .email) + validations.add("password", as: String.self, is: .count(8...)) + } +} +``` + +Agora você pode criar o endpoint `POST /users`. + +```swift +app.post("users") { req async throws -> User in + try User.Create.validate(content: req) + let create = try req.content.decode(User.Create.self) + guard create.password == create.confirmPassword else { + throw Abort(.badRequest, reason: "As senhas não correspondem") + } + let user = try User( + name: create.name, + email: create.email, + passwordHash: Bcrypt.hash(create.password) + ) + try await user.save(on: req.db) + return user +} +``` + +Este endpoint valida a requisição recebida, decodifica a struct `User.Create` e verifica se as senhas correspondem. Então usa os dados decodificados para criar um novo `User` e salva no banco de dados. A senha em texto puro é hasheada usando `Bcrypt` antes de salvar no banco de dados. + +Compile e execute o projeto, certificando-se de migrar o banco de dados primeiro, e então use a seguinte requisição para criar um novo usuário. + +```http +POST /users HTTP/1.1 +Content-Length: 97 +Content-Type: application/json + +{ + "name": "Vapor", + "email": "test@vapor.codes", + "password": "secret42", + "confirmPassword": "secret42" +} +``` + +#### Model Authenticatable + +Agora que você tem um modelo de usuário e um endpoint para criar novos usuários, vamos conformar o modelo com `ModelAuthenticatable`. Isso permitirá que o modelo seja autenticado usando nome de usuário e senha. + +```swift +import Fluent +import Vapor + +extension User: ModelAuthenticatable { + static let usernameKey = \User.$email + static let passwordHashKey = \User.$passwordHash + + func verify(password: String) throws -> Bool { + try Bcrypt.verify(password, created: self.passwordHash) + } +} +``` + +Esta extensão adiciona conformidade com `ModelAuthenticatable` ao `User`. As duas primeiras propriedades especificam quais campos devem ser usados para armazenar o nome de usuário e o hash de senha, respectivamente. A notação `\` cria um key path para os campos que o Fluent pode usar para acessá-los. + +O último requisito é um método para verificar senhas em texto puro enviadas no header de autenticação Basic. Como estamos usando Bcrypt para hashear a senha durante o cadastro, usaremos Bcrypt para verificar se a senha fornecida corresponde ao hash de senha armazenado. + +Agora que o `User` conforma com `ModelAuthenticatable`, podemos criar um authenticator para proteger a rota de login. + +```swift +let passwordProtected = app.grouped(User.authenticator()) +passwordProtected.post("login") { req -> User in + try req.auth.require(User.self) +} +``` + +`ModelAuthenticatable` adiciona um método estático `authenticator` para criar um authenticator. + +Teste se esta rota funciona enviando a seguinte requisição. + +```http +POST /login HTTP/1.1 +Authorization: Basic dGVzdEB2YXBvci5jb2RlczpzZWNyZXQ0Mg== +``` + +Esta requisição passa o nome de usuário `test@vapor.codes` e a senha `secret42` via header de autenticação Basic. Você deverá ver o usuário criado anteriormente sendo retornado. + +Embora você pudesse teoricamente usar autenticação Basic para proteger todos os seus endpoints, é recomendado usar um token separado. Isso minimiza a frequência com que você precisa enviar a senha sensível do usuário pela Internet. Também torna a autenticação muito mais rápida, já que você só precisa realizar o hashing de senha durante o login. + +### User Token + +Crie um novo modelo para representar tokens de usuário. + +```swift +import Fluent +import Vapor + +final class UserToken: Model, Content { + static let schema = "user_tokens" + + @ID(key: .id) + var id: UUID? + + @Field(key: "value") + var value: String + + @Parent(key: "user_id") + var user: User + + init() { } + + init(id: UUID? = nil, value: String, userID: User.IDValue) { + self.id = id + self.value = value + self.$user.id = userID + } +} +``` + +Este modelo deve ter um campo `value` para armazenar a string única do token. Também deve ter uma [relação parent](../fluent/overview.md#parent) com o modelo de usuário. Você pode adicionar propriedades adicionais a este token como desejar, como uma data de expiração. + +Em seguida, crie uma migration para este modelo. + +```swift +import Fluent + +extension UserToken { + struct Migration: AsyncMigration { + var name: String { "CreateUserToken" } + + func prepare(on database: Database) async throws { + try await database.schema("user_tokens") + .id() + .field("value", .string, .required) + .field("user_id", .uuid, .required, .references("users", "id")) + .unique(on: "value") + .create() + } + + func revert(on database: Database) async throws { + try await database.schema("user_tokens").delete() + } + } +} +``` + +Note que esta migration torna o campo `value` único. Também cria uma referência de chave estrangeira entre o campo `user_id` e a tabela users. + +Não esqueça de adicionar a migration a `app.migrations`. + +```swift +app.migrations.add(UserToken.Migration()) +``` + +Finalmente, adicione um método no `User` para gerar um novo token. Este método será usado durante o login. + +```swift +extension User { + func generateToken() throws -> UserToken { + try .init( + value: [UInt8].random(count: 16).base64, + userID: self.requireID() + ) + } +} +``` + +Aqui estamos usando `[UInt8].random(count:)` para gerar um valor de token aleatório. Para este exemplo, 16 bytes, ou 128 bits, de dados aleatórios estão sendo usados. Você pode ajustar este número como desejar. Os dados aleatórios são então codificados em base-64 para facilitar a transmissão em headers HTTP. + +Agora que você pode gerar tokens de usuário, atualize a rota `POST /login` para criar e retornar um token. + +```swift +let passwordProtected = app.grouped(User.authenticator()) +passwordProtected.post("login") { req async throws -> UserToken in + let user = try req.auth.require(User.self) + let token = try user.generateToken() + try await token.save(on: req.db) + return token +} +``` + +Teste se esta rota funciona usando a mesma requisição de login acima. Agora você deverá receber um token ao fazer login que se parece com algo assim: + +``` +8gtg300Jwdhc/Ffw784EXA== +``` + +Guarde o token que você recebeu, pois o usaremos em breve. + +#### Model Token Authenticatable + +Conforme `UserToken` com `ModelTokenAuthenticatable`. Isso permitirá que tokens autentiquem seu modelo `User`. + +```swift +import Vapor +import Fluent + +extension UserToken: ModelTokenAuthenticatable { + static var valueKey: KeyPath> { \.$value } + static var userKey: KeyPath> { \.$user } + + var isValid: Bool { + true + } +} +``` + +O primeiro requisito do protocolo especifica qual campo armazena o valor único do token. Este é o valor que será enviado no header de autenticação Bearer. O segundo requisito especifica a relação parent com o modelo `User`. É assim que o Fluent buscará o usuário autenticado. + +O requisito final é um booleano `isValid`. Se for `false`, o token será deletado do banco de dados e o usuário não será autenticado. Para simplicidade, tornaremos os tokens eternos fixando isso como `true`. + +Agora que o token conforma com `ModelTokenAuthenticatable`, você pode criar um authenticator para proteger rotas. + +Crie um novo endpoint `GET /me` para obter o usuário autenticado atual. + +```swift +let tokenProtected = app.grouped(UserToken.authenticator()) +tokenProtected.get("me") { req -> User in + try req.auth.require(User.self) +} +``` + +Similar ao `User`, `UserToken` agora tem um método estático `authenticator()` que pode gerar um authenticator. O authenticator tentará encontrar um `UserToken` correspondente usando o valor fornecido no header de autenticação Bearer. Se encontrar uma correspondência, buscará o `User` relacionado e o autenticará. + +Teste se esta rota funciona enviando a seguinte requisição HTTP onde o token é o valor que você salvou da requisição `POST /login`. + +```http +GET /me HTTP/1.1 +Authorization: Bearer +``` + +Você deverá ver o `User` autenticado sendo retornado. + +## Sessão + +A [API de Sessão](../advanced/sessions.md) do Vapor pode ser usada para persistir automaticamente a autenticação do usuário entre requisições. Isso funciona armazenando um identificador único para o usuário nos dados de sessão da requisição após um login bem-sucedido. Em requisições subsequentes, o identificador do usuário é buscado da sessão e usado para autenticar o usuário antes de chamar seu route handler. + +Sessões são ótimas para aplicações web front-end construídas no Vapor que servem HTML diretamente para navegadores web. Para APIs, recomendamos usar autenticação stateless baseada em token para persistir dados do usuário entre requisições. + +### Session Authenticatable + +Para usar autenticação baseada em sessão, você precisará de um tipo que conforme com `SessionAuthenticatable`. Para este exemplo, usaremos uma struct simples. + +```swift +import Vapor + +struct User { + var email: String +} +``` + +Para conformar com `SessionAuthenticatable`, você precisará especificar um `sessionID`. Este é o valor que será armazenado nos dados da sessão e deve identificar o usuário de forma única. + +```swift +extension User: SessionAuthenticatable { + var sessionID: String { + self.email + } +} +``` + +Para nosso tipo simples `User`, usaremos o endereço de e-mail como identificador único de sessão. + +### Session Authenticator + +Em seguida, precisaremos de um `SessionAuthenticator` para lidar com a resolução de instâncias do nosso User a partir do identificador de sessão persistido. + +```swift +struct UserSessionAuthenticator: SessionAuthenticator { + typealias User = App.User + func authenticate(sessionID: String, for request: Request) -> EventLoopFuture { + let user = User(email: sessionID) + request.auth.login(user) + return request.eventLoop.makeSucceededFuture(()) + } +} +``` + +Se você estiver usando `async`/`await`, pode usar o `AsyncSessionAuthenticator`: + +```swift +struct UserSessionAuthenticator: AsyncSessionAuthenticator { + typealias User = App.User + func authenticate(sessionID: String, for request: Request) async throws { + let user = User(email: sessionID) + request.auth.login(user) + } +} +``` + +Como todas as informações que precisamos para inicializar nosso exemplo `User` estão contidas no identificador de sessão, podemos criar e logar o usuário de forma síncrona. Em uma aplicação real, você provavelmente usaria o identificador de sessão para realizar uma consulta ao banco de dados ou requisição de API para buscar o restante dos dados do usuário antes de autenticar. + +Em seguida, vamos criar um bearer authenticator simples para realizar a autenticação inicial. + +```swift +struct UserBearerAuthenticator: AsyncBearerAuthenticator { + func authenticate(bearer: BearerAuthorization, for request: Request) async throws { + if bearer.token == "test" { + let user = User(email: "hello@vapor.codes") + request.auth.login(user) + } + } +} +``` + +Este authenticator autenticará um usuário com o e-mail `hello@vapor.codes` quando o bearer token `test` for enviado. + +Finalmente, vamos combinar todas essas peças na sua aplicação. + +```swift +// Criar grupo de rotas protegidas que requer autenticação de usuário. +let protected = app.routes.grouped([ + app.sessions.middleware, + UserSessionAuthenticator(), + UserBearerAuthenticator(), + User.guardMiddleware(), +]) + +// Adicionar rota GET /me para ler o e-mail do usuário. +protected.get("me") { req -> String in + try req.auth.require(User.self).email +} +``` + +`SessionsMiddleware` é adicionado primeiro para habilitar o suporte a sessões na aplicação. Mais informações sobre configuração de sessões podem ser encontradas na seção [API de Sessão](../advanced/sessions.md). + +Em seguida, o `SessionAuthenticator` é adicionado. Ele lida com a autenticação do usuário se uma sessão estiver ativa. + +Se a autenticação ainda não foi persistida na sessão, a requisição será encaminhada para o próximo authenticator. `UserBearerAuthenticator` verificará o bearer token e autenticará o usuário se for igual a `"test"`. + +Finalmente, `User.guardMiddleware()` garantirá que `User` foi autenticado por um dos middleware anteriores. Se o usuário não foi autenticado, um erro será lançado. + +Para testar esta rota, primeiro envie a seguinte requisição: + +```http +GET /me HTTP/1.1 +authorization: Bearer test +``` + +Isso fará com que `UserBearerAuthenticator` autentique o usuário. Uma vez autenticado, `UserSessionAuthenticator` persistirá o identificador do usuário no armazenamento de sessão e gerará um cookie. Use o cookie da resposta em uma segunda requisição à rota. + +```http +GET /me HTTP/1.1 +cookie: vapor_session=123 +``` + +Desta vez, `UserSessionAuthenticator` autenticará o usuário e você deverá ver novamente o e-mail do usuário sendo retornado. + +### Model Session Authenticatable + +Modelos Fluent podem gerar `SessionAuthenticator`s conformando com `ModelSessionAuthenticatable`. Isso usará o identificador único do modelo como o identificador de sessão e realizará automaticamente uma consulta ao banco de dados para restaurar o modelo a partir da sessão. + +```swift +import Fluent + +final class User: Model { ... } + +// Permitir que este modelo seja persistido em sessões. +extension User: ModelSessionAuthenticatable { } +``` + +Você pode adicionar `ModelSessionAuthenticatable` a qualquer modelo existente como uma conformidade vazia. Uma vez adicionado, um novo método estático estará disponível para criar um `SessionAuthenticator` para aquele modelo. + +```swift +User.sessionAuthenticator() +``` + +Isso usará o banco de dados padrão da aplicação para resolver o usuário. Para especificar um banco de dados, passe o identificador. + +```swift +User.sessionAuthenticator(.sqlite) +``` + +## Autenticação de Website + +Websites são um caso especial para autenticação porque o uso de um navegador restringe como você pode anexar credenciais a uma requisição. Isso leva a dois cenários diferentes de autenticação: + +* o login inicial via formulário +* chamadas subsequentes autenticadas com um cookie de sessão + +O Vapor e o Fluent fornecem vários helpers para tornar isso transparente. + +### Autenticação de Sessão + +A autenticação de sessão funciona como descrito acima. Você precisa aplicar o middleware de sessão e o session authenticator a todas as rotas que seu usuário acessará. Isso inclui quaisquer rotas protegidas, quaisquer rotas públicas onde você ainda possa querer acessar o usuário caso esteja logado (para exibir um botão de conta, por exemplo) **e** rotas de login. + +Você pode habilitar isso globalmente na sua aplicação em **configure.swift** assim: + +```swift +app.middleware.use(app.sessions.middleware) +app.middleware.use(User.sessionAuthenticator()) +``` + +Esses middleware fazem o seguinte: + +* o middleware de sessões pega o cookie de sessão fornecido na requisição e o converte em uma sessão +* o session authenticator pega a sessão e verifica se há um usuário autenticado para aquela sessão. Se sim, o middleware autentica a requisição. Na resposta, o session authenticator verifica se a requisição tem um usuário autenticado e o salva na sessão para que esteja autenticado na próxima requisição. + +!!! note "Nota" + O cookie de sessão não é definido como `secure` e `httpOnly` por padrão. Consulte a [API de Sessão](../advanced/sessions.md#configuration) do Vapor para mais informações sobre como configurar cookies. + +### Protegendo Rotas + +Ao proteger rotas para uma API, você tradicionalmente retorna uma resposta HTTP com um código de status como **401 Unauthorized** se a requisição não estiver autenticada. No entanto, isso não é uma boa experiência para alguém usando um navegador. O Vapor fornece um `RedirectMiddleware` para qualquer tipo `Authenticatable` para usar neste cenário: + +```swift +let protectedRoutes = app.grouped(User.redirectMiddleware(path: "/login?loginRequired=true")) +``` + +O objeto `RedirectMiddleware` também suporta passar uma closure que retorna o caminho de redirecionamento como uma `String` durante a criação para tratamento avançado de URL. Por exemplo, incluindo o caminho redirecionado como parâmetro de query no alvo de redirecionamento para gerenciamento de estado. + +```swift +let redirectMiddleware = User.redirectMiddleware { req -> String in + return "/login?authRequired=true&next=\(req.url.path)" +} +``` + +Isso funciona de forma similar ao `GuardMiddleware`. Quaisquer requisições para rotas registradas em `protectedRoutes` que não estejam autenticadas serão redirecionadas para o caminho fornecido. Isso permite que você diga aos seus usuários para fazer login, em vez de apenas fornecer um **401 Unauthorized**. + +Certifique-se de incluir um Session Authenticator antes do `RedirectMiddleware` para garantir que o usuário autenticado seja carregado antes de passar pelo `RedirectMiddleware`. + +```swift +let protectedRoutes = app.grouped([User.sessionAuthenticator(), redirectMiddleware]) +``` + +### Login via Formulário + +Para autenticar um usuário e requisições futuras com uma sessão, você precisa logar um usuário. O Vapor fornece um protocolo `ModelCredentialsAuthenticatable` para conformar. Isso lida com login via formulário. Primeiro conforme seu `User` com este protocolo: + +```swift +extension User: ModelCredentialsAuthenticatable { + static let usernameKey = \User.$email + static let passwordHashKey = \User.$password + + func verify(password: String) throws -> Bool { + try Bcrypt.verify(password, created: self.password) + } +} +``` + +Isso é idêntico a `ModelAuthenticatable` e se você já conforma com ele, não precisa fazer mais nada. Em seguida, aplique este middleware `ModelCredentialsAuthenticator` à sua requisição POST do formulário de login: + +```swift +let credentialsProtectedRoute = sessionRoutes.grouped(User.credentialsAuthenticator()) +credentialsProtectedRoute.post("login", use: loginPostHandler) +``` + +Isso usa o credentials authenticator padrão para proteger a rota de login. Você deve enviar `username` e `password` na requisição POST. Você pode configurar seu formulário assim: + +```html +
    + + + + + +
    +``` + +O `CredentialsAuthenticator` extrai o `username` e `password` do corpo da requisição, encontra o usuário pelo nome de usuário e verifica a senha. Se a senha for válida, o middleware autentica a requisição. O `SessionAuthenticator` então autentica a sessão para requisições subsequentes. + +## JWT + +O [JWT](jwt.md) fornece um `JWTAuthenticator` que pode ser usado para autenticar JSON Web Tokens em requisições recebidas. Se você é novo em JWT, confira a [visão geral](jwt.md). + +Primeiro, crie um tipo representando um payload JWT. + +```swift +// Exemplo de payload JWT. +struct SessionToken: Content, Authenticatable, JWTPayload { + + // Constantes + let expirationTime: TimeInterval = 60 * 15 + + // Dados do Token + var expiration: ExpirationClaim + var userId: UUID + + init(userId: UUID) { + self.userId = userId + self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime)) + } + + init(with user: User) throws { + self.userId = try user.requireID() + self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime)) + } + + func verify(using algorithm: some JWTAlgorithm) throws { + try expiration.verifyNotExpired() + } +} +``` + +Em seguida, podemos definir uma representação dos dados contidos em uma resposta de login bem-sucedida. Por enquanto, a resposta terá apenas uma propriedade que é uma string representando um JWT assinado. + +```swift +struct ClientTokenResponse: Content { + var token: String +} +``` + +Usando nosso modelo para o token JWT e resposta, podemos usar uma rota de login protegida por senha que retorna um `ClientTokenResponse` e inclui um `SessionToken` assinado. + +```swift +let passwordProtected = app.grouped(User.authenticator(), User.guardMiddleware()) +passwordProtected.post("login") { req async throws -> ClientTokenResponse in + let user = try req.auth.require(User.self) + let payload = try SessionToken(with: user) + return ClientTokenResponse(token: try await req.jwt.sign(payload)) +} +``` + +Alternativamente, se você não quiser usar um authenticator, pode ter algo parecido com o seguinte. +```swift +app.post("login") { req async throws -> ClientTokenResponse in + // Validar credenciais fornecidas para o usuário + // Obter userId para o usuário fornecido + let payload = try SessionToken(userId: userId) + return ClientTokenResponse(token: try await req.jwt.sign(payload)) +} +``` + +Ao conformar o payload com `Authenticatable` e `JWTPayload`, você pode gerar um route authenticator usando o método `authenticator()`. Adicione isso a um grupo de rotas para buscar e verificar automaticamente o JWT antes que sua rota seja chamada. + +```swift +// Criar um grupo de rotas que requer o JWT SessionToken. +let secure = app.grouped(SessionToken.authenticator(), SessionToken.guardMiddleware()) +``` + +Adicionar o [guard middleware](#guard-middleware) opcional exigirá que a autorização tenha sido bem-sucedida. + +Dentro das rotas protegidas, você pode acessar o payload JWT autenticado usando `req.auth`. + +```swift +// Retornar resposta ok se o token fornecido pelo usuário for válido. +secure.post("validateLoggedInUser") { req -> HTTPStatus in + let sessionToken = try req.auth.require(SessionToken.self) + print(sessionToken.userId) + return .ok +} +``` diff --git a/docs/security/crypto.pt.md b/docs/security/crypto.pt.md new file mode 100644 index 000000000..eb8b27453 --- /dev/null +++ b/docs/security/crypto.pt.md @@ -0,0 +1,93 @@ +# Criptografia + +O Vapor inclui o [SwiftCrypto](https://github.com/apple/swift-crypto/), que é um port compatível com Linux da biblioteca CryptoKit da Apple. Algumas APIs criptográficas adicionais são expostas para funcionalidades que o SwiftCrypto ainda não possui, como [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) e [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm). + +## SwiftCrypto + +A biblioteca `Crypto` do Swift implementa a API CryptoKit da Apple. Sendo assim, a [documentação do CryptoKit](https://developer.apple.com/documentation/cryptokit) e a [palestra da WWDC](https://developer.apple.com/videos/play/wwdc2019/709) são ótimos recursos para aprender a API. + +Essas APIs estarão disponíveis automaticamente quando você importar o Vapor. + +```swift +import Vapor + +let digest = SHA256.hash(data: Data("hello".utf8)) +print(digest) +``` + +O CryptoKit inclui suporte para: + +- Hashing: `SHA512`, `SHA384`, `SHA256` +- Message Authentication Codes: `HMAC` +- Ciphers: `AES`, `ChaChaPoly` +- Public-Key Cryptography: `Curve25519`, `P521`, `P384`, `P256` +- Hashing inseguro: `SHA1`, `MD5` + +## Bcrypt + +Bcrypt é um algoritmo de hashing de senhas que usa um salt aleatório para garantir que o hash da mesma senha múltiplas vezes não resulte no mesmo digest. + +O Vapor fornece um tipo `Bcrypt` para hashing e comparação de senhas. + +```swift +import Vapor + +let digest = try Bcrypt.hash("test") +``` + +Como o Bcrypt usa um salt, hashes de senha não podem ser comparados diretamente. Tanto a senha em texto puro quanto o digest existente devem ser verificados juntos. + +```swift +import Vapor + +let pass = try Bcrypt.verify("test", created: digest) +if pass { + // Senha e digest correspondem. +} else { + // Senha incorreta. +} +``` + +O login com senhas Bcrypt pode ser implementado buscando primeiro o digest da senha do usuário no banco de dados por e-mail ou nome de usuário. O digest conhecido pode então ser verificado contra a senha em texto puro fornecida. + +## OTP + +O Vapor suporta senhas de uso único HOTP e TOTP. OTPs funcionam com as funções de hash SHA-1, SHA-256 e SHA-512 e podem fornecer seis, sete ou oito dígitos de saída. Um OTP fornece autenticação gerando uma senha de uso único legível por humanos. Para isso, as partes primeiro concordam em uma chave simétrica, que deve ser mantida privada em todos os momentos para manter a segurança das senhas geradas. + +#### HOTP + +HOTP é um OTP baseado em uma assinatura HMAC. Além da chave simétrica, ambas as partes também concordam em um contador, que é um número que fornece unicidade para a senha. Após cada tentativa de geração, o contador é incrementado. +```swift +let key = SymmetricKey(size: .bits128) +let hotp = HOTP(key: key, digest: .sha256, digits: .six) +let code = hotp.generate(counter: 25) + +// Ou usando a função estática generate +HOTP.generate(key: key, digest: .sha256, digits: .six, counter: 25) +``` + +#### TOTP + +Um TOTP é uma variação baseada em tempo do HOTP. Funciona basicamente da mesma forma, mas em vez de um simples contador, o horário atual é usado para gerar unicidade. Para compensar a inevitável defasagem introduzida por relógios não sincronizados, latência de rede, atraso do usuário e outros fatores, um código TOTP gerado permanece válido por um intervalo de tempo especificado (mais comumente, 30 segundos). +```swift +let key = SymmetricKey(size: .bits128) +let totp = TOTP(key: key, digest: .sha256, digits: .six, interval: 60) +let code = totp.generate(time: Date()) + +// Ou usando a função estática generate +TOTP.generate(key: key, digest: .sha256, digits: .six, interval: 60, time: Date()) +``` + +#### Range +OTPs são muito úteis para fornecer tolerância na validação e contadores fora de sincronização. Ambas as implementações de OTP têm a capacidade de gerar um OTP com uma margem de erro. +```swift +let key = SymmetricKey(size: .bits128) +let hotp = HOTP(key: key, digest: .sha256, digits: .six) + +// Gerar uma janela de contadores corretos +let codes = hotp.generate(counter: 25, range: 2) +``` +O exemplo acima permite uma margem de 2, o que significa que o HOTP será calculado para os valores de contador `23 ... 27`, e todos esses códigos serão retornados. + +!!! warning "Aviso" + Nota: Quanto maior a margem de erro utilizada, mais tempo e liberdade um atacante tem para agir, diminuindo a segurança do algoritmo. diff --git a/docs/security/jwt.pt.md b/docs/security/jwt.pt.md new file mode 100644 index 000000000..c3c5bcd8e --- /dev/null +++ b/docs/security/jwt.pt.md @@ -0,0 +1,437 @@ +# JWT + +JSON Web Token (JWT) é um padrão aberto ([RFC 7519](https://tools.ietf.org/html/rfc7519)) que define uma forma compacta e autocontida para transmitir informações de forma segura entre partes como um objeto JSON. Essas informações podem ser verificadas e confiáveis porque são assinadas digitalmente. + +JWTs são particularmente úteis em aplicações web, onde são comumente usados para autenticação/autorização stateless e troca de informações. Você pode ler mais sobre a teoria por trás dos JWTs na especificação acima ou em [jwt.io](https://jwt.io/introduction). + +O Vapor fornece suporte de primeira classe para JWTs através do módulo `JWT`. Este módulo é construído sobre a biblioteca `JWTKit`, que é uma implementação Swift do padrão JWT baseada no [SwiftCrypto](https://github.com/apple/swift-crypto). O JWTKit fornece signers e verifiers para uma variedade de algoritmos, incluindo HMAC, ECDSA, EdDSA e RSA. + +## Primeiros Passos + +O primeiro passo para usar JWTs na sua aplicação Vapor é adicionar a dependência `JWT` ao arquivo `Package.swift` do seu projeto: + +```swift +// swift-tools-version:5.10 +import PackageDescription + +let package = Package( + name: "my-app", + dependencies: [ + // Outras dependências... + .package(url: "https://github.com/vapor/jwt.git", from: "5.0.0"), + ], + targets: [ + .target(name: "App", dependencies: [ + // Outras dependências... + .product(name: "JWT", package: "jwt") + ]), + // Outros targets... + ] +) +``` + +### Configuração + +Após adicionar a dependência, você pode começar a usar o módulo `JWT` na sua aplicação. O módulo JWT adiciona uma nova propriedade `jwt` à `Application` que é usada para configuração, cujos internos são fornecidos pela biblioteca [JWTKit](https://github.com/vapor/jwt-kit). + +#### Key Collection + +O objeto `jwt` possui uma propriedade `keys`, que é uma instância de `JWTKeyCollection` do JWTKit. Esta coleção é usada para armazenar e gerenciar as chaves usadas para assinar e verificar JWTs. A `JWTKeyCollection` é um `actor`, o que significa que todas as operações na coleção são serializadas e thread-safe. + +Para assinar ou verificar JWTs, você precisará adicionar uma chave à coleção. Isso geralmente é feito no seu arquivo `configure.swift`: + +```swift +import JWT + +// Adicionar signer HMAC com SHA-256. +await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256) +``` + +Isso adiciona uma chave HMAC com SHA-256 como algoritmo de digest ao keychain, ou HS256 na notação JWA. Confira a seção de [algoritmos](#algoritmos) abaixo para mais informações sobre os algoritmos disponíveis. + +!!! note "Nota" + Certifique-se de substituir `"secret"` por uma chave secreta real. Esta chave deve ser mantida segura, idealmente em um arquivo de configuração ou variável de ambiente. + +### Assinatura + +A chave adicionada pode então ser usada para assinar JWTs. Para fazer isso, você primeiro precisa de _algo_ para assinar, ou seja, um 'payload'. +Este payload é simplesmente um objeto JSON contendo os dados que você quer transmitir. Você pode criar seu payload personalizado conformando sua estrutura ao protocolo `JWTPayload`: + +```swift +// Estrutura do payload JWT. +struct TestPayload: JWTPayload { + // Mapeia os nomes de propriedade Swift mais longos para as + // chaves abreviadas usadas no payload JWT. + enum CodingKeys: String, CodingKey { + case subject = "sub" + case expiration = "exp" + case isAdmin = "admin" + } + + // A claim "sub" (subject) identifica o principal que é o + // sujeito do JWT. + var subject: SubjectClaim + + // A claim "exp" (expiration time) identifica o tempo de expiração + // após o qual o JWT NÃO DEVE ser aceito para processamento. + var expiration: ExpirationClaim + + // Dados personalizados. + // Se verdadeiro, o usuário é um admin. + var isAdmin: Bool + + // Execute qualquer lógica de verificação adicional além + // da verificação de assinatura aqui. + // Como temos uma ExpirationClaim, vamos + // chamar seu método verify. + func verify(using algorithm: some JWTAlgorithm) async throws { + try self.expiration.verifyNotExpired() + } +} +``` + +A assinatura do payload é feita chamando o método `sign` no módulo `JWT`, por exemplo dentro de um route handler: + +```swift +app.post("login") { req async throws -> [String: String] in + let payload = TestPayload( + subject: "vapor", + expiration: .init(value: .distantFuture), + isAdmin: true + ) + return try await ["token": req.jwt.sign(payload)] +} +``` + +Quando uma requisição é feita a este endpoint, ele retornará o JWT assinado como uma `String` no corpo da resposta, e se tudo correu conforme o planejado, você verá algo assim: + +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo" +} +``` + +Você pode decodificar e verificar este token usando o [debugger do `jwt.io`](https://jwt.io/#debugger). O debugger mostrará o payload (que deve ser os dados que você especificou anteriormente) e o header do JWT, e você pode verificar a assinatura usando a chave secreta que usou para assinar o JWT. + +### Verificação + +Quando um token é enviado _para_ a sua aplicação, você pode verificar a autenticidade do token chamando o método `verify` no módulo `JWT`: + +```swift +// Buscar e verificar JWT da requisição recebida. +app.get("me") { req async throws -> HTTPStatus in + let payload = try await req.jwt.verify(as: TestPayload.self) + print(payload) + return .ok +} +``` + +O helper `req.jwt.verify` verificará o header `Authorization` em busca de um bearer token. Se existir, ele fará o parse do JWT e verificará sua assinatura e claims. Se qualquer uma dessas etapas falhar, um erro 401 Unauthorized será lançado. + +Teste a rota enviando a seguinte requisição HTTP: + +```http +GET /me HTTP/1.1 +authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo +``` + +Se tudo funcionou, uma resposta `200 OK` será retornada e o payload será impresso: + +```swift +TestPayload( + subject: "vapor", + expiration: 4001-01-01 00:00:00 +0000, + isAdmin: true +) +``` + +O fluxo completo de autenticação pode ser encontrado em [Autenticação → JWT](authentication.md#jwt). + +## Algoritmos + +JWTs podem ser assinados usando uma variedade de algoritmos. + +Para adicionar uma chave ao keychain, uma sobrecarga do método `add` está disponível para cada um dos seguintes algoritmos: + +### HMAC + +HMAC (Hash-based Message Authentication Code) é um algoritmo simétrico que usa uma chave secreta para assinar e verificar o JWT. O Vapor suporta os seguintes algoritmos HMAC: + +- `HS256`: HMAC com SHA-256 +- `HS384`: HMAC com SHA-384 +- `HS512`: HMAC com SHA-512 + +```swift +// Adicionar uma chave HS256. +await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256) +``` + +### ECDSA + +ECDSA (Elliptic Curve Digital Signature Algorithm) é um algoritmo assimétrico que usa um par de chaves pública/privada para assinar e verificar o JWT. Sua confiabilidade é baseada na matemática em torno de curvas elípticas. O Vapor suporta os seguintes algoritmos ECDSA: + +- `ES256`: ECDSA com curva P-256 e SHA-256 +- `ES384`: ECDSA com curva P-384 e SHA-384 +- `ES512`: ECDSA com curva P-521 e SHA-512 + +Todos os algoritmos fornecem tanto uma chave pública quanto uma chave privada, como `ES256PublicKey` e `ES256PrivateKey`. Você pode adicionar chaves ECDSA usando o formato PEM: + +```swift +let ecdsaPublicKey = """ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx +C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ== +-----END PUBLIC KEY----- +""" + +// Inicializar uma chave ECDSA com PEM público. +let key = try ES256PublicKey(pem: ecdsaPublicKey) +``` + +ou gerar aleatórias (útil para testes): + +```swift +let key = ES256PrivateKey() +``` + +Para adicionar a chave ao keychain: + +```swift +await app.jwt.keys.add(ecdsa: key) +``` + +### EdDSA + +EdDSA (Edwards-curve Digital Signature Algorithm) é um algoritmo assimétrico que usa um par de chaves pública/privada para assinar e verificar o JWT. É similar ao ECDSA na medida em que ambos dependem do algoritmo DSA, mas o EdDSA é baseado na curva de Edwards, uma família diferente de curvas elípticas, e tem leves melhorias de desempenho. No entanto, também é mais novo e, portanto, menos amplamente suportado. O Vapor suporta apenas o algoritmo `EdDSA` que usa a curva `Ed25519`. + +Você pode criar uma chave EdDSA usando sua coordenada (uma `String` codificada em base-64), então `x` se for uma chave pública e `d` se for uma chave privada: + +```swift +let publicKey = try EdDSA.PublicKey(x: "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE", curve: .ed25519) + +let privateKey = try EdDSA.PrivateKey(d: "d1H3/dcg0V3XyAuZW2TE5Z3rhY20M+4YAfYu/HUQd8w=", curve: .ed25519) +``` + +Você também pode gerar aleatórias: + +```swift +let key = EdDSA.PrivateKey(curve: .ed25519) +``` + +Para adicionar a chave ao keychain: + +```swift +await app.jwt.keys.add(eddsa: key) +``` + +### RSA + +RSA (Rivest-Shamir-Adleman) é um algoritmo assimétrico que usa um par de chaves pública/privada para assinar e verificar o JWT. + +!!! warning "Aviso" + Como você verá, as chaves RSA estão protegidas por um namespace `Insecure` para desencorajar novos usuários de utilizá-las. Isso porque o RSA é considerado menos seguro que ECDSA e EdDSA, e deve ser usado apenas por razões de compatibilidade. + Se possível, use qualquer um dos outros algoritmos. + +O Vapor suporta os seguintes algoritmos RSA: + +- `RS256`: RSA com SHA-256 +- `RS384`: RSA com SHA-384 +- `RS512`: RSA com SHA-512 + +Você pode criar uma chave RSA usando o formato PEM: + +```swift +let rsaPublicKey = """ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx +PmjXpbCkecAWLj/CcDWEcuTZkYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv +7FPo5Cq8FkvrdDzeacwRSxYuIq1LtYnd6I30qNaNthntjvbqyMmBulJ1mzLI+Xg/ +aX4rbSL49Z3dAQn8vQIDAQAB +-----END PUBLIC KEY----- +""" + +// Inicializar uma chave RSA com PEM público. +let key = try Insecure.RSA.PublicKey(pem: rsaPublicKey) +``` + +ou usando seus componentes: + +```swift +// Inicializar uma chave RSA privada com componentes. +let key = try Insecure.RSA.PrivateKey( + modulus: modulus, + exponent: publicExponent, + privateExponent: privateExponent +) +``` + +!!! warning "Aviso" + O pacote não suporta chaves RSA menores que 2048 bits. + +Então você pode adicionar a chave à coleção de chaves: + +```swift +await app.jwt.keys.add(rsa: key, digestAlgorithm: .sha256) +``` + +### PSS + +Além do algoritmo RSA-PKCS1v1.5, o Vapor também suporta o algoritmo RSA-PSS. PSS (Probabilistic Signature Scheme) é um esquema de preenchimento mais seguro para assinaturas RSA. É recomendado usar PSS em vez de PKCS1v1.5 quando possível. + +O algoritmo difere apenas na fase de assinatura, o que significa que as chaves são as mesmas que RSA, porém, você precisa especificar o esquema de preenchimento ao adicioná-las à coleção de chaves: + +```swift +await app.jwt.keys.add(pss: key, digestAlgorithm: .sha256) +``` + +## Identificador de Chave (kid) + +Ao adicionar uma chave à coleção de chaves, você também pode especificar um identificador de chave (kid). Este é um identificador único para a chave que pode ser usado para buscar a chave na coleção. + +```swift +// Adicionar chave HMAC com SHA-256 chamada "a". +await app.jwt.keys.add(hmac: "foo", digestAlgorithm: .sha256, kid: "a") +``` + +Se você não especificar um `kid`, a chave será atribuída como a chave padrão. + +!!! note "Nota" + A chave padrão será substituída se você adicionar outra chave sem um `kid`. + +Ao assinar um JWT, você pode especificar o `kid` a ser usado: + +```swift +let token = try await req.jwt.sign(payload, kid: "a") +``` + +Ao verificar, por outro lado, o `kid` é automaticamente extraído do header do JWT e usado para buscar a chave na coleção. Há também um parâmetro `iteratingKeys` no método verify que permite especificar se deve iterar sobre todas as chaves na coleção caso o `kid` não seja encontrado. + +## Claims + +O pacote JWT do Vapor inclui vários helpers para implementar [claims JWT](https://tools.ietf.org/html/rfc7519#section-4.1) comuns. + +|Claim|Tipo|Método de Verificação| +|---|---|---| +|`aud`|`AudienceClaim`|`verifyIntendedAudience(includes:)`| +|`exp`|`ExpirationClaim`|`verifyNotExpired(currentDate:)`| +|`jti`|`IDClaim`|n/a| +|`iat`|`IssuedAtClaim`|n/a| +|`iss`|`IssuerClaim`|n/a| +|`locale`|`LocaleClaim`|n/a| +|`nbf`|`NotBeforeClaim`|`verifyNotBefore(currentDate:)`| +|`sub`|`SubjectClaim`|n/a| + +Todas as claims devem ser verificadas no método `JWTPayload.verify`. Se a claim tiver um método de verificação especial, você pode usá-lo. Caso contrário, acesse o valor da claim usando `value` e verifique se ele é válido. + +## JWK + +Um JSON Web Key (JWK) é uma estrutura de dados JSON que representa uma chave criptográfica ([RFC7517](https://datatracker.ietf.org/doc/html/rfc7517)). Estes são comumente usados para fornecer chaves aos clientes para verificação de JWTs. + +Por exemplo, a Apple hospeda seus JWKS do Sign in with Apple na seguinte URL. + +```http +GET https://appleid.apple.com/auth/keys +``` + +O Vapor fornece utilitários para adicionar JWKs à coleção de chaves: + +```swift +let privateKey = """ +{ + "kty": "RSA", + "d": "\(rsaPrivateExponent)", + "e": "AQAB", + "use": "sig", + "kid": "1234", + "alg": "RS256", + "n": "\(rsaModulus)" +} +""" + +let jwk = try JWK(json: privateKey) +try await app.jwt.keys.use(jwk: jwk) +``` + +Isso adicionará o JWK à coleção de chaves, e você pode usá-lo para assinar e verificar JWTs como faria com qualquer outra chave. + +### JWKs + +Se você tem múltiplos JWKs, pode adicioná-los da mesma forma: + +```swift +let json = """ +{ + "keys": [ + {"kty": "RSA", "alg": "RS256", "kid": "a", "n": "\(rsaModulus)", "e": "AQAB"}, + {"kty": "RSA", "alg": "RS512", "kid": "b", "n": "\(rsaModulus)", "e": "AQAB"}, + ] +} +""" + +try await app.jwt.keys.use(jwksJSON: json) +``` + +## Vendors + +O Vapor fornece APIs para lidar com JWTs dos emissores populares abaixo. + +### Apple + +Primeiro, configure o identificador da sua aplicação Apple. + +```swift +// Configurar identificador do app Apple. +app.jwt.apple.applicationIdentifier = "..." +``` + +Então, use o helper `req.jwt.apple` para buscar e verificar um JWT da Apple. + +```swift +// Buscar e verificar JWT da Apple do header Authorization. +app.get("apple") { req async throws -> HTTPStatus in + let token = try await req.jwt.apple.verify() + print(token) // AppleIdentityToken + return .ok +} +``` + +### Google + +Primeiro, configure o identificador da sua aplicação Google e o nome de domínio G Suite. + +```swift +// Configurar identificador do app Google e nome de domínio. +app.jwt.google.applicationIdentifier = "..." +app.jwt.google.gSuiteDomainName = "..." +``` + +Então, use o helper `req.jwt.google` para buscar e verificar um JWT do Google. + +```swift +// Buscar e verificar JWT do Google do header Authorization. +app.get("google") { req async throws -> HTTPStatus in + let token = try await req.jwt.google.verify() + print(token) // GoogleIdentityToken + return .ok +} +``` + +### Microsoft + +Primeiro, configure o identificador da sua aplicação Microsoft. + +```swift +// Configurar identificador do app Microsoft. +app.jwt.microsoft.applicationIdentifier = "..." +``` + +Então, use o helper `req.jwt.microsoft` para buscar e verificar um JWT da Microsoft. + +```swift +// Buscar e verificar JWT da Microsoft do header Authorization. +app.get("microsoft") { req async throws -> HTTPStatus in + let token = try await req.jwt.microsoft.verify() + print(token) // MicrosoftIdentityToken + return .ok +} +``` diff --git a/docs/security/passwords.pt.md b/docs/security/passwords.pt.md new file mode 100644 index 000000000..bffb09c8d --- /dev/null +++ b/docs/security/passwords.pt.md @@ -0,0 +1,87 @@ +# Senhas + +O Vapor inclui uma API de hashing de senhas para ajudar você a armazenar e verificar senhas de forma segura. Essa API é configurável com base no ambiente e suporta hashing assíncrono. + +## Configuração + +Para configurar o hasher de senhas da Application, use `app.passwords`. + +```swift +import Vapor + +app.passwords.use(...) +``` + +### Bcrypt + +Para usar a [API Bcrypt](crypto.md#bcrypt) do Vapor para hashing de senhas, especifique `.bcrypt`. Este é o padrão. + +```swift +app.passwords.use(.bcrypt) +``` + +O Bcrypt usará um custo de 12, a menos que seja especificado de outra forma. Você pode configurar isso passando o parâmetro `cost`. + +```swift +app.passwords.use(.bcrypt(cost: 8)) +``` + +### Plaintext + +O Vapor inclui um hasher de senhas inseguro que armazena e verifica senhas como texto puro. Isso não deve ser usado em produção, mas pode ser útil para testes. + +```swift +switch app.environment { +case .testing: + app.passwords.use(.plaintext) +default: break +} +``` + +## Hashing + +Para fazer hash de senhas, use o helper `password` disponível no `Request`. + +```swift +let digest = try req.password.hash("vapor") +``` + +Os digests de senha podem ser verificados contra a senha em texto puro usando o método `verify`. + +```swift +let bool = try req.password.verify("vapor", created: digest) +``` + +A mesma API está disponível na `Application` para uso durante a inicialização. + +```swift +let digest = try app.password.hash("vapor") +``` + +### Async + +Os algoritmos de hashing de senha são projetados para serem lentos e intensivos em CPU. Por causa disso, você pode querer evitar bloquear o event loop durante o hashing de senhas. O Vapor fornece uma API assíncrona de hashing de senhas que despacha o hashing para um pool de threads em background. Para usar a API assíncrona, use a propriedade `async` em um hasher de senhas. + +```swift +req.password.async.hash("vapor").map { digest in + // Tratar digest. +} + +// ou + +let digest = try await req.password.async.hash("vapor") +``` + +A verificação de digests funciona de forma similar: + +```swift +req.password.async.verify("vapor", created: digest).map { bool in + // Tratar resultado. +} + +// ou + +let result = try await req.password.async.verify("vapor", created: digest) +``` + +Calcular hashes em threads de background pode liberar os event loops da sua aplicação para lidar com mais requisições. diff --git a/docs/version/legacy-docs.pt.md b/docs/version/legacy-docs.pt.md new file mode 100644 index 000000000..9285a1185 --- /dev/null +++ b/docs/version/legacy-docs.pt.md @@ -0,0 +1,3 @@ +# Redirecionando... + + diff --git a/mkdocs.yml b/mkdocs.yml index 4df2f495b..fdd749657 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -482,6 +482,65 @@ plugins: Welcome: Witaj Xcode: Xcode site_name: Dokumentacja Vapor + - build: true + default: false + locale: pt + name: Português Brasileiro + nav_translations: + APNS: APNS + Advanced: Avançado + Async: Async + Authentication: Autenticação + Basics: Fundamentos + Client: Cliente + Commands: Comandos + Content: Conteúdo + Contributing: Contribuindo + Contributing Guide: Guia de Contribuição + Controllers: Controladores + Crypto: Criptografia + Custom Tags: Tags Personalizadas + Deploy: Deploy + Environment: Ambiente + Errors: Erros + Files: Arquivos + Fluent: Fluent + Folder Structure: Estrutura de Pastas + Getting Started: Primeiros Passos + Hello, world: Olá, Mundo + Install: Instalação + JWT: JWT + Leaf: Leaf + Legacy Docs: Documentação Legada + Logging: Logging + Middleware: Middleware + Migrations: Migrações + Model: Modelo + Overview: Visão Geral + Passwords: Senhas + Query: Consultas + Queues: Filas + Redis: Redis + Relations: Relações + Release Notes: Notas de Lançamento + Request: Requisição + Routing: Rotas + Schema: Schema + Security: Segurança + Server: Servidor + Services: Serviços + Sessions: Sessões + SwiftPM: SwiftPM + Testing: Testes + Tracing: Rastreamento + Transactions: Transações + Upgrading: Atualizando + Validation: Validação + Version (4.0): Versão (4.0) + WebSockets: WebSockets + Welcome: Bem-vindo + Xcode: Xcode + site_name: Documentação do Vapor - build: true default: false locale: zh