Rupert  Beatty

Rupert Beatty

1673010960

Mailgun: Service to Assist with Sending Emails From Vapor Apps

Mailgun

Mailgun is a Vapor 4 service for a popular email sending API

Note: Vapor3 version is available in vapor3 branch and from 3.0.0 tag

Installation

Mailgun can be installed with Swift Package Manager

.package(url: "https://github.com/vapor-community/mailgun.git", from: "5.0.0")

.target(name: "App", dependencies: [
    .product(name: "Vapor", package: "vapor"),
    .product(name: "Mailgun", package: "mailgun")
])

Usage

Sign up and set up a Mailgun account here

Make sure you get an API key and register a custom domain

Configure

In configure.swift:

import Mailgun

// Called before your application initializes.
func configure(_ app: Application) throws {
    /// case 1
    /// put into your environment variables the following keys:
    /// MAILGUN_API_KEY=...
    app.mailgun.configuration = .environment

    /// case 2
    /// manually
    app.mailgun.configuration = .init(apiKey: "<api key>")
}

Note: If your private api key begins with key-, be sure to include it

Declare all your domains

extension MailgunDomain {
    static var myApp1: MailgunDomain { .init("mg.myapp1.com", .us) }
    static var myApp2: MailgunDomain { .init("mg.myapp2.com", .eu) }
    static var myApp3: MailgunDomain { .init("mg.myapp3.com", .us) }
    static var myApp4: MailgunDomain { .init("mg.myapp4.com", .eu) }
}

Set default domain in configure.swift

app.mailgun.defaultDomain = .myApp1

Usage

Mailgun is available on both Application and Request

// call it without arguments to use default domain
app.mailgun().send(...)
req.mailgun().send(...)

// or call it with domain
app.mailgun(.myApp1).send(...)
req.mailgun(.myApp1).send(...)

In configure.swift

import Mailgun

// Called before your application initializes.
func configure(_ app: Application) throws {
    /// configure mailgun

    /// then you're ready to use it
    app.mailgun(.myApp1).send(...).whenSuccess { response in
        print("just sent: \(response)")
    }
}

πŸ’‘ NOTE: All the examples below will be with Request, but you could do the same with Application as in example above.

In routes.swift:

Without attachments

import Mailgun

func routes(_ app: Application) throws {
    app.post("mail") { req -> EventLoopFuture<ClientResponse> in
        let message = MailgunMessage(
            from: "postmaster@example.com",
            to: "example@gmail.com",
            subject: "Newsletter",
            text: "This is a newsletter",
            html: "<h1>This is a newsletter</h1>"
        )
        return req.mailgun().send(message)
    }
}

With attachments

import Mailgun

func routes(_ app: Application) throws {
    app.post("mail") { req -> EventLoopFuture<ClientResponse> in
        let fm = FileManager.default
        guard let attachmentData = fm.contents(atPath: "/tmp/test.pdf") else {
          throw Abort(.internalServerError)
        }
        let bytes: [UInt8] = Array(attachmentData)
        var bytesBuffer = ByteBufferAllocator().buffer(capacity: bytes.count)
        bytesBuffer.writeBytes(bytes)
        let attachment = File.init(data: bytesBuffer, filename: "test.pdf")
        let message = MailgunMessage(
            from: "postmaster@example.com",
            to: "example@gmail.com",
            subject: "Newsletter",
            text: "This is a newsletter",
            html: "<h1>This is a newsletter</h1>",
            attachments: [attachment]
        )
        return req.mailgun().send(message)
    }
}

With template (attachments can be used in same way)

import Mailgun

func routes(_ app: Application) throws {
    app.post("mail") { req -> EventLoopFuture<ClientResponse> in
        let message = MailgunTemplateMessage(
            from: "postmaster@example.com",
            to: "example@gmail.com",
            subject: "Newsletter",
            template: "my-template",
            templateData: ["foo": "bar"]
        )
        return req.mailgun().send(message)
    }
}

Setup content through Leaf

Using Vapor Leaf, you can easily setup your HTML Content.

First setup a leaf file in Resources/Views/Emails/my-email.leaf

<html>
    <body>
        <p>Hi #(name)</p>
    </body>
</html>

With this, you can change the #(name) with a variable from your Swift code, when sending the mail

import Mailgun

func routes(_ app: Application) throws {
    app.post("mail") { req -> EventLoopFuture<ClientResponse> in
        let content = try req.view().render("Emails/my-email", [
            "name": "Bob"
        ])

        let message = Mailgun.Message(
            from: "postmaster@example.com",
            to: "example@gmail.com",
            subject: "Newsletter",
            text: "",
            html: content
        )

        return req.mailgun().send(message)
    }
}

Setup routes

public func configure(_ app: Application) throws {
    // sets up a catch_all forward for the route listed
    let routeSetup = MailgunRouteSetup(forwardURL: "http://example.com/mailgun/all", description: "A route for all emails")
    app.mailgun().setup(forwarding: routeSetup).whenSuccess { response in
        print(response)
    }
}

Handle routes

import Mailgun

func routes(_ app: Application) throws {
    let mailgunGroup = app.grouped("mailgun")
    mailgunGroup.post("all") { req -> String in
        do {
            let incomingMail = try req.content.decode(MailgunIncomingMessage.self)
            print("incomingMail: (incomingMail)")
            return "Hello"
        } catch {
            throw Abort(.internalServerError, reason: "Could not decode incoming message")
        }
    }
}

Creating templates

import Mailgun

func routes(_ app: Application) throws {
    let mailgunGroup = app.grouped("mailgun")
    mailgunGroup.post("template") { req -> EventLoopFuture<ClientResponse> in
        let template = MailgunTemplate(name: "my-template", description: "api created :)", template: "<h1>Hello {{ name }}</h1>")
        return req.mailgun().createTemplate(template)
    }
}

Download Details:

Author: Vapor-community
Source Code: https://github.com/vapor-community/mailgun 

#swift #email #vapor 

Mailgun: Service to Assist with Sending Emails From Vapor Apps
Rupert  Beatty

Rupert Beatty

1673007060

Swiftybeaver-provider: SwiftyBeaver Logging Provider for Vapor

SwiftyBeaver Logging Provider for Vapor

Adds the powerful logging of SwiftyBeaver to Vapor for server-side Swift 4 on Linux and Mac.

Installation

Add this project to the Package.swift dependencies of your Vapor project:

  .package(url: "https://github.com/vapor-community/swiftybeaver-provider.git", from: "3.0.0")

Setup

After you've added the SwiftyBeaver Provider package to your project, setting the provider up in code is easy.

You can configure your SwiftyBeaver instance in a pure swift way or using a JSON file like Vapor 2 do,

Register using a pure swift way

import SwiftBeaverProvider

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services ) throws {
    // ...

    // Setup your destinations
    let console = ConsoleDestination()
    console.minLevel = .info // update properties according to your needs

    let fileDestination = FileDestination()

    // Register the logger
    services.register(SwiftyBeaverLogger(destinations: [console, fileDestination]), as: Logger.self)

    // Optional
    config.prefer(SwiftyBeaverLogger.self, for: Logger.self)
}

Register using a JSON file

First, register the SwiftyBeaverProvider in your configure.swift file.

import SwiftBeaverProvider

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services ) throws {
    // ...

    // Register providers first
    try services.register(SwiftyBeaverProvider())

    // Optional
    config.prefer(SwiftyBeaverLogger.self, for: Logger.self)
}

Configure Destinations

If you run your application now, you will likely see an error that the SwiftyBeaver configuration file is missing. Let's add that now

The configuration consist of an array of destinations located in Config/swiftybeaver.json file. Here is an example of a simple SwiftyBeaver configuration file to configure console, file and swiftybeaver platform destinations with their required properties.

[
  {
    "type": "console",
    "format": " $Dyyyy-MM-dd HH:mm:ss.SSS$d $C$L$c: $M"
  },
  {
    "type": "file"
  },
  {
    "type": "platform",
    "app": "YOUR_APP_ID",
    "secret": "YOUR_SECRET_ID",
    "key": "YOUR_ENCRYPTION_KEY"
  }
]

Aditional options:

KEYAVAILABLE FORTYPEOBSERVATION
asyncconsole, fileBool 
formatconsole, fileStringA space must be placed before dollar sign
levelString.debugconsole, fileString 
levelString.errorconsole, fileString 
levelString.infoconsole, fileString 
levelString.verboseconsole, fileString 
levelString.warningconsole, fileString 
pathfileStringpath to the log file
minLevelconsole, file, platformStringvalues: verbose, debug, info, warning, error
thresholdplatformIntmin: 1, max: 1000

Note: 
It's a good idea to store the SwiftyBeaver configuration file in the Config/secrets folder since it contains sensitive information. 

To get more information about configuration options check the official SwiftyBeaver docs

Use

router.get("hello") { req -> Future<String> in
    // Get a logger instance
    let logger: Logger = try req.make(SwiftyBeaverLogger.self)

    // Or
    let logger: Logger = try req.make(Logger.self) // needs config.prefer(SwiftyBeaverLogger.self, for: Logger.self) in configure.swift

    logger.info("Logger info")
    return Future("Hello, world!")
}

Please also see the SwiftyBeaver destination docs and how to set a custom logging format

 

Output to Xcode 8 Console

Learn more about colored logging to Xcode 8 Console. 

 

Output to File

Learn more about logging to file which is great for Terminal.app fans or to store logs on disk. 

 

Output to Cloud & Mac App

swiftybeaver-demo1

Learn more about logging to the SwiftyBeaver Platform during release! 

 

Learn More

Get support via Github Issues, email and our public Slack channel

Credits

This package is developed and maintained by Gustavo Perdomo with the collaboration of all vapor community.

Download Details:

Author: Vapor-community
Source Code: https://github.com/vapor-community/swiftybeaver-provider 
License: MIT license

#swift #logging #vapor 

Swiftybeaver-provider: SwiftyBeaver Logging Provider for Vapor
Rupert  Beatty

Rupert Beatty

1673003040

Sugar: A Package Of Sugar for Vapor

Sugar 🍬

πŸ“¦ Installation

Add Sugar to the package dependencies (in your Package.swift file):

dependencies: [
    ...,
    .package(url: "https://github.com/nodes-vapor/sugar.git", from: "4.0.0")
]

as well as to your target (e.g. "App"):

targets: [
    ...
    .target(
        name: "App",
        dependencies: [... "Sugar" ...]
    ),
    ...
]

Getting started πŸš€

Make sure that you've imported Sugar everywhere when needed:

import Sugar

Helpers

This package contains a lot of misc. functionality that might not fit into it's own package or that would best to get PR'ed into Vapor. Some examples of what this package contains:

Environment variables

Access environment variables by writing

env("my-key", "my-fallback-value")

Seeder commands

If you want to make your model seedable, you can conform it to Seedable and use SeederCommand to wrap your seedable model. This basically means that you can focus on how your model gets initialized when running your command, and save a little code on actually performing the database work.

Seeding multiple instances of your model will be added - feel free to PR.

Authentication

This package contains a lot of convenience related to JWT, usernames and passwords which is used in JWTKeychain and Admin Panel.

Migrations

Sugar contains a helper function for adding properties while excluding some specific ones. This makes it a bit more convenient if you want to only modify how a single one or a couple of fields gets prepared.

extension MyModel: Migration {
    static func prepare(on connection: MySQLConnection) -> Future<Void> {
        return MySQLDatabase.create(self, on: connection) { builder in
            try addProperties(to: builder, excluding: [\.title])
            builder.field(for: \.title, type: .varchar(191))
        }
    }
}

πŸ† Credits

This package is developed and maintained by the Vapor team at Nodes.

Download Details:

Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/sugar 
License: MIT license

#swift #vapor #server 

Sugar: A Package Of Sugar for Vapor
Rupert  Beatty

Rupert Beatty

1672999200

Provides A Common Structure to Deal with Data Based API Requests

Submissions πŸ“©

Installation

Package.swift

Add Submissions to the Package dependencies:

dependencies: [
    ...,
    .package(url: "https://github.com/nodes-vapor/submissions.git", from: "3.0.0")
]

as well as to your target (e.g. "App"):

targets: [
    ...
    .target(
        name: "App",
        dependencies: [
            ...
            .product(name: "Submissions", package: "submissions")
        ]
    ),
    ...
]

Introduction

Submissions was written to reduce the amount of boilerplate needed to write the common tasks of rendering forms and processing and validating data from POST/PUT/PATCH requests (PPP-request, or submission for short). Submissions makes it easy to present detailed validation errors for web users as well as API consumers.

Submissions is designed to be flexible. Its functionality is based around Fields which are abstractions that model the parts of a submission.

single values with its validators and meta data such as a label. Usually a form or API request involves multiple properties comprising a model. This can be modeled using multiple Fields.

Getting started πŸš€

First make sure that you've imported Submissions everywhere it's needed:

import Submissions

Adding the Provider

"Submissions" comes with a light-weight provider that we'll need to register in the configure function in our configure.swift file:

try services.register(SubmissionsProvider())

This makes sure that fields and errors can be stored on the request using a FieldCache service.

Validating API requests

TODO

Validating HTML form requests

Submissions comes with leaf tags that can render fields into HTML. The leaf files needs to be copied from the folder Resources/Views/Submissions from Submissions to your project's Resources/Views. Then we can register Submissions' leaf tags where you register your other leaf tags, for instance:

var leafTagConfig = LeafTagConfig.default()
...
leafTagConfig.useSubmissionsLeafTags()
services.register(leafTagConfig)

You can customize where Submissions looks for the leaf tags by passing in a modified instance of TagTemplatePaths to useSubmissionsLeafTags(paths:).

In order to render a view that contains Submissions leaf tags we need to ensure that the Fields are added to the field cache and that the Request is passed into the render call:

let nameField = Field(key: "name", value: "", label: "Name")
try req.fieldCache().addFields([nameField])
try req.view().render("index", on: req)

In your leaf file you can then refer to this field using an appropriate tag and the key "name" as defined when creating the Field.

Tags

Input tags

The following input tags are available for your leaf files.

#submissions:checkbox( ... )
#submissions:email( ... )
#submissions:hidden( ... )
#submissions:password( ... )
#submissions:text( ... )
#submissions:textarea( ... )

They all accept the same number of parameters.

With these options:

PositionTypeDescriptionExampleRequired?
1keyKey to the related field in the field cache"name"yes
2placeholderPlaceholder text"Enter name"no
3help textHelp text"This name will be visible to others"no

File tag

To add a file upload to your form use this leaf tag.

#submissions:file( ... )

With these options:

PositionTypeDescriptionExampleRequired?
1keyKey to the related field in the field cache"avatar"yes
2help textHelp text"This will replace your existing avatar"no
3acceptPlaceholder text"image/*"no
4multipleSupport multple file uploads"true" (or any other non-nil value)no

Select tag

A select tag can be added as follows.

#submissions:select( ... )

With these options:

PositionTypeDescriptionExampleRequired?
1keyKey to the related field in the field cache"role"yes
2optionsThe possible options in the drop downrolesno
3placeholderPlaceholder text"Select an role"no
4help textHelp text"The role defines the actions a user is allowed to perform"no

The second option (e.g. roles) is a special parameter that defines the dropdown options. It has to be passed into the render call something like this.

enum Role: String, CaseIterable, Codable {
    case user, admin, superAdmin
}

extension Role: OptionRepresentable {
    var optionID: String? {
        return self.rawValue
    }

    var optionValue: String? {
        return self.rawValue.uppercased()
    }
}

let roles: [Role] = .
try req.view().render("index", ["roles": roles.allCases.makeOptions()] on: req)

πŸ† Credits

This package is developed and maintained by the Vapor team at Nodes.

Download Details:

Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/submissions 
License: MIT license

#swift #validation #vapor 

Provides A Common Structure to Deal with Data Based API Requests
Rupert  Beatty

Rupert Beatty

1672995060

Storage: Eases The Use Of Multiple Storage and CDN Services

Storage πŸ—„

A package to ease the use of multiple storage and CDN services.

πŸ“¦ Installation

Add Storage to the package dependencies (in your Package.swift file):

dependencies: [
    ...,
    .package(url: "https://github.com/nodes-vapor/storage.git", from: "1.0.0")
]

as well as to your target (e.g. "App"):

targets: [
    ...
    .target(
        name: "App",
        dependencies: [... "Storage" ...]
    ),
    ...
]

Getting started πŸš€

Storage makes it easy to start uploading and downloading files. Just register a network driver and get going.

Upload a file 🌐

There are a few different interfaces for uploading a file, the simplest being the following:

Storage.upload(
    bytes: [UInt8],
    fileName: String?,
    fileExtension: String?,
    mime: String?,
    folder: String,
    on container: Container 
) throws -> String

The aforementioned function will attempt to upload the file using your selected driver and template and will return a String representing the location of the file.

If you want to upload an image named profile.png your call site would look like:

try Storage.upload(
    bytes: bytes,
    fileName: "profile.png",
    on: req
)

Base64 and data URI πŸ“‘

Is your file a base64 or data URI? No problem!

Storage.upload(base64: "SGVsbG8sIFdvcmxkIQ==", fileName: "base64.txt", on: req)
Storage.upload(dataURI: "data:,Hello%2C%20World!", fileName: "data-uri.txt", on: req)

Remote resources

Download an asset from a URL and then reupload it to your storage server.

Storage.upload(url: "http://mysite.com/myimage.png", fileName: "profile.png", on: req)

Download a file βœ…

To download a file that was previously uploaded you simply use the generated path.

// download image as `Foundation.Data`
let data = try Storage.get("/images/profile.png", on: req)

Get CDN path

In order to use the CDN path convenience, you'll have to set the CDN base url on Storage, e.g. in your configure.swift file:

Storage.cdnBaseURL = "https://cdn.vapor.cloud"

Here is how you generate the CDN path to a given asset.

let cdnPath = try Storage.getCDNPath(for: path)

If your CDN path is more involved than cdnUrl + path, you can build out Storage's optional completionhandler to override the default functionality.

Storage.cdnPathBuilder = { baseURL, path in
    let joinedPath = (baseURL + path)
    return joinedPath.replacingOccurrences(of: "/images/original/", with: "/image/")
}

Delete a file ❌

Deleting a file can be done as follows.

try Storage.delete("/images/profile.png")

Configuration βš™

Storage has a variety of configurable options.

Network driver πŸ”¨

The network driver is the module responsible for interacting with your 3rd party service. The default, and currently the only, driver is s3.

import Storage

let driver = try S3Driver(
    bucket: "bucket", 
    accessKey: "access",
    secretKey: "secret"
)

services.register(driver, as: NetworkDriver.self)

bucket, accessKeyand secretKey are required by the S3 driver, while template, host and region are optional. region will default to eu-west-1 and host will default to s3.amazonaws.com.

Upload path πŸ›£

A times, you may need to upload files to a different scheme than /file.ext. You can achieve this by passing in the pathTemplate parameter when creating the S3Driver. If the parameter is omitted it will default to /#file.

The following template will upload profile.png from the folder images to /myapp/images/profile.png

let driver = try S3Driver(
    bucket: "mybucket",
    accessKey: "myaccesskey",
    secretKey: "mysecretkey",
    pathTemplate: "/myapp/#folder/#file"
)

Aliases

Aliases are special keys in your template that will be replaced with dynamic information at the time of upload.

Note: if you use an alias and the information wasn't provided at the file upload's callsite, Storage will throw a missingX/malformedX error.

#file: The file's name and extension.

File: "test.png"
Returns: test.png

#fileName: The file's name.

File: "test.png"
Returns: test

#fileExtension: The file's extension.

File: "test.png"
Returns: png

#folder: The provided folder.

File: "uploads/test.png"
Returns: uploads

#mime: The file's content type.

File: "test.png"
Returns: image/png

#mimeFolder: A folder generated according to the file's mime.

This alias will check the file's mime and if it's an image, it will return images/original else it will return data

File: "test.png"
Returns: images/original

#day: The current day.

File: "test.png"
Date: 12/12/2012
Returns: 12

#month: The current month.

File: "test.png"
Date: 12/12/2012
Returns: 12

#year: The current year.

File: "test.png"
Date: 12/12/2012
Returns: 2012

#timestamp: The time of upload.

File: "test.png"
Time: 17:05:00
Returns: 17:05:00

#uuid: A generated UUID.

File: "test.png"
Returns: 123e4567-e89b-12d3-a456-426655440000

πŸ† Credits

This package is developed and maintained by the Vapor team at Nodes.

Download Details:

Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/storage 
License: MIT license

#swift #storage #vapor #upload 

Storage: Eases The Use Of Multiple Storage and CDN Services
Rupert  Beatty

Rupert Beatty

1672991220

Slugify: Convenience for Sluggifying Your Strings

Slugify πŸ”€

πŸ“¦ Installation

This package is independent of Vapor and can be used for all Swift projects up til version 4.2

Update your Package.swift file.

.package(url: "https://github.com/nodes-vapor/slugify", from: "2.0.0")

Getting started πŸš€

import Slugify
print("My test URL æøΓ₯".slugify())

The above code will print: my-test-url-aeoa

πŸ† Credits

This package is developed and maintained by the Vapor team at Nodes. The package owner for this project is John.

Download Details:

Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/slugify 
License: MIT license

#swift #vapor #server

Slugify: Convenience for Sluggifying Your Strings
Rupert  Beatty

Rupert Beatty

1672987440

A Swift Vapor provider for simple file logging on Vapor sites

Overview

A simple Vapor Logger provider for outputting server logs to log files.

Simple File Logger outputs separate files based on the log's LogLevel. Debug logs are output to debug.log, error logs to error.log, and so on. By default, logs are output to:

LinuxmacOS
/var/log/Vapor/~/Library/Caches/Vapor/

You can change Vapor/ to an arbitrary directory by changing the executableName during setup.

Installation

Add this dependency to your Package.swift:

dependencies: [
    .package(url: "https://github.com/hallee/vapor-simple-file-logger.git", from: "1.0.1"),
],

And add "SimpleFileLogger" as a dependency to your app's target.

Setup

In configure.swift:

services.register(SimpleFileLogger.self)
config.prefer(SimpleFileLogger.self, for: Logger.self)

To define an executable name and include timestamps, you can provide configuration:

services.register(Logger.self) { container -> SimpleFileLogger in
    return SimpleFileLogger(executableName: "hal.codes", 
                            includeTimestamps: true)
}
config.prefer(SimpleFileLogger.self, for: Logger.self)

Usage

You can create a logger anywhere in your Vapor application with access to its Container with:

Container.make(Logger.self)

For example, to log all the requests to your server:

router.get(PathComponent.catchall) { req in
    let logger = try? req.sharedContainer.make(Logger.self)
    logger?.debug(req.description)
}

Download Details:

Author: Hallee
Source Code: https://github.com/hallee/vapor-simple-file-logger 
License: MIT license

#swift #vapor #logger 

A Swift Vapor provider for simple file logging on Vapor sites
Rupert  Beatty

Rupert Beatty

1672983480

Sendgrid: SendGrid-powered Mail Backend for Vapor

SendGrid Provider for Vapor

Adds a mail backend for SendGrid to the Vapor web framework. Send simple emails, or leverage the full capabilities of SendGrid's V3 API.

Setup

Add the dependency to Package.swift:

.package(url: "https://github.com/vapor-community/sendgrid.git", from: "4.0.0")

Make sure SENDGRID_API_KEY is set in your environment. This can be set in the Xcode scheme, or specified in your docker-compose.yml, or even provided as part of a swift run command.

Optionally, explicitly initialize the provider (this is strongly recommended, as otherwise a missing API key will cause a fatal error some time later in your application):

app.sendgrid.initialize()

Now you can access the client at any time:

app.sendgrid.client

Using the API

You can use all of the available parameters here to build your SendGridEmail Usage in a route closure would be as followed:

import SendGrid

let email = SendGridEmail(…)

return req.application.sendgrid.client.send([email], on: req.eventLoop)

Error handling

If the request to the API failed for any reason a SendGridError is the result of the future, and has an errors property that contains an array of errors returned by the API:

return req.application.sendgrid.client.send([email], on: req.eventLoop).flatMapError { error in
    if let sendgridError = error as? SendGridError {
        req.logger.error("\(error)")
    }
    // ...
}

Download Details:

Author: Vapor-community
Source Code: https://github.com/vapor-community/sendgrid 
License: MIT license

#swift #email #vapor #sendgrid 

Sendgrid: SendGrid-powered Mail Backend for Vapor
Rupert  Beatty

Rupert Beatty

1672979700

Sanitize: Powerful Model Extraction From Vapor JSON Requests

Sanitize

Powerful model extraction from JSON requests.

Installation

Add this project to the Package.swift dependencies of your Vapor project:

  .Package(url: "https://github.com/gperdomor/sanitize.git", majorVersion: 1)

or for Swift 4:

  .package(url: "https://github.com/gperdomor/sanitize.git", from: "1.0.0")

Usage

Model

Before you're able to extract your model from a request it needs to conform to the protocol Sanitizable adding a [String] named allowedKeys with a list of keys you wish to allow:

import Sanitize

class User: Sanitizable { // or struct
    var id: Node?
    var name: String
    var email: String

    // Valid properties taken from the request json
    static var allowedKeys: [String] = ["name", "email"]

    //...
}

Now that you have a conforming model, you can safely extract it from a Request

Request Body

{
  "id": 1,
  "name": "John Appleseed",
  "email": "example@domain.com"
}

Routes

drop.post("model") { req in
    var user: User = try req.extractModel()
    print(user.id == nil) // prints `true` because was removed (`id` is not a allowed key)
    try user.save()
    return user
}

Pre and Post validations

You can also configure some preSanitize and postSanitize validations, this validations will be executed before and after model initialization.

extension User {
    static func preSanitize(data: JSON) throws {
        guard data["name"]?.string != nil else {
            throw Abort(
                .badRequest,
                metadata: nil,
                reason: "No name provided."
            )
        }

        guard data["email"]?.string != nil else {
            throw Abort(
                .badRequest,
                metadata: nil,
                reason: "No email provided."
            )
        }
    }

    func postSanitize() throws {
        guard email.characters.count > 8 else {
            throw Abort(
                .badRequest,
                metadata: nil,
                reason: "Email must be longer than 8 characters."
            )
        }
    }
}

Credits

This package is developed and maintained by Gustavo Perdomo.

This package is heavily inspired by Sanitized

Download Details:

Author: Gperdomor
Source Code: https://github.com/gperdomor/sanitize 
License: MIT license

#swift #vapor 

Sanitize: Powerful Model Extraction From Vapor JSON Requests
Rupert  Beatty

Rupert Beatty

1672971960

Paginator: Offset pagination for Vapor

Paginator πŸ—‚

This package currently offers support for offset pagination on Array and QueryBuilder.

GIF of paginator

πŸ“¦ Installation

Add Paginator to the package dependencies (in your Package.swift file):

dependencies: [
    ...,
    .package(url: "https://github.com/nodes-vapor/paginator.git", from: "4.0.0")
]

as well as to your target (e.g. "App"):

targets: [
    ...
    .target(
        name: "App",
        dependencies: [... "Paginator" ...]
    ),
    ...
]

Next, copy/paste the Resources/Views/Paginator folder into your project in order to be able to use the provided Leaf tags. These files can be changed as explained in the Leaf Tags section, however it's recommended to copy this folder to your project anyway. This makes it easier for you to keep track of updates and your project will work if you decide later on to not use your own customized leaf files.

Getting started πŸš€

First make sure that you've imported Paginator everywhere it's needed:

import Paginator

Adding the Leaf tag

In order to do pagination in Leaf, please make sure to add the Leaf tag:

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    services.register { _ -> LeafTagConfig in
        var tags = LeafTagConfig.default()
        tags.use([
            "offsetPaginator": OffsetPaginatorTag(templatePath: "Paginator/offsetpaginator")
        ])

        return tags
    }
}

If you want to fully customize the way the pagination control are being generated, you are free to override the template path.

QueryBuilder

To return a paginated result from QueryBuilder, you can do the following:

router.get("galaxies") { (req: Request) -> Future<OffsetPaginator<Galaxy>> in
    return Galaxy.query(on: req).paginate(on: req)
}

Array

For convenience, Paginator also comes with support for paginating Array:

router.get("galaxies") { (req: Request) -> Future<OffsetPaginator<Galaxy>> in
    let galaxies = [Galaxy(), Galaxy(), Galaxy()]
    return galaxies.paginate(on: req)
}

RawSQL

Paginator also comes with support for paginating raw SQL queries for complex expressions not compatible with Fluent.

Simple example using PostgreSQL:

router.get("galaxies") { (req: Request) -> Future<OffsetPaginator<Galaxy>> in
    return req.withPooledConnection(to: .psql) { conn -> Future<OffsetPaginator<Galaxy>> in
        let rawBuilder = RawSQLBuilder<PostgreSQLDatabase, Galaxy>(
            query: """
                SELECT *
                FROM public."Galaxy"
            """, countQuery: """
                SELECT COUNT(*) as "count"
                FROM public."Galaxy"
            """, connection: conn)
        return try rawBuilder.paginate(on: req)
    }
}

Note: the count query is expected to have a result with one column named count and with the total columns value in the first row

Leaf tags

To use Paginator together with Leaf, you can do the following:

struct GalaxyList: Codable {
    let galaxies: [Galaxy]
}

router.get("galaxies") { (req: Request) -> Response in
    let paginator: Future<OffsetPaginator<Galaxy>> = Galaxy.query(on: req).paginate(on: req)
    return paginator.flatMap(to: Response.self) { paginator in
        return try req.view().render(
            "MyLeafFile", 
            GalaxyList(galaxies: paginator.data ?? []), 
            userInfo: try paginator.userInfo(),
            on: req
        )
        .encode(for: req)
    }
}

Please note how the Paginator data is being passed in using userInfo on the render call. Forgetting to pass this in will result in an error being thrown.

Then in your MyLeafFile.leaf you could do something like:

<ul>
    #for(galaxy in galaxies) {
        <li>#(galaxy.name)</li>
    }
</ul>

#offsetPaginator()

Calling the Leaf tag for OffsetPaginator will automatically generate the Bootstrap 4 HTML for showing the pagination controls:

<nav class="paginator">
    <ul class="pagination justify-content-center table-responsive">
        <li class="page-item">
            <a href="/admin/users?page=16" class="page-link" rel="prev" aria-label="Previous">
                <span aria-hidden="true">Β«</span>
                <span class="sr-only">Previous</span>
            </a>
        </li>
        <li class="page-item "><a href="/admin/users?page=1" class="page-link">1</a></li>
        <li class="disabled page-item"><a href="#" class="page-link">...</a></li>
        <li class="page-item "><a href="" class="page-link">12</a></li>
        <li class="page-item "><a href="" class="page-link">13</a></li>
        <li class="page-item "><a href="" class="page-link">14</a></li>
        <li class="page-item "><a href="" class="page-link">15</a></li>
        <li class="page-item "><a href="" class="page-link">16</a></li>
        <li class="page-item  active "><a href="" class="page-link">17</a></li>
        <li class="page-item "><a href="/admin/users?page=18" class="page-link">18</a></li>
        <li class="page-item">
            <a href="/admin/users?page=18" class="page-link" rel="next" aria-label="Next">
                <span aria-hidden="true">Β»</span>
                <span class="sr-only">Next</span>
            </a>
        </li>
    </ul>
</nav>

Transforming

The data in an OffsetPaginator can be transformed by mapping over the paginator and transforming each element at a time:

Galaxy.query(on: req).paginate(on: req).map { paginator in
    paginator.map { (galaxy: Galaxy) -> GalaxyViewModel in 
        GalaxyViewModel(galaxy: galaxy)
    }
}

You can also transform a whole page of results at once:

Galaxy.query(on: req).paginate(on: req).map { paginator in
    paginator.map { (galaxies: [Galaxy]) -> [GalaxyViewModel] in 
        galaxies.map(GalaxyViewModel.init)
    }
}

In case the transformation requires async work you can do:

Galaxy.query(on: req).paginate(on: req).map { paginator in
    paginator.flatMap { (galaxies: [Galaxy]) -> Future<[GalaxyViewModel]> in 
        galaxies.someAsyncMethod()
    }
}

Configuration

The OffsetPaginator has a configuration file (OffsetPaginatorConfig) that can be overwritten if needed. This can be done in configure.swift:

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    // ..
    services.register(OffsetPaginatorConfig(
        perPage: 1,
        defaultPage: 1
    ))
}

πŸ† Credits

This package is developed and maintained by the Vapor team at Nodes.

Download Details:

Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/paginator 
License: MIT license

#swift #vapor #server 

Paginator: Offset pagination for Vapor
Rupert  Beatty

Rupert Beatty

1672968180

Pagination: Simple Vapor 3 Pagination

Vapor Pagination

Pagination is based off of the Fluent 2 pagination system.

Getting Started

Add this to your Package.swift file

.package(url: "https://github.com/vapor-community/pagination.git", from: "1.0.0")

Conform your model to Paginatable

extension MyModel: Paginatable { }

Once you have done that, it's as simple as returning your query in paginated format.

func test(_ req: Request) throws -> Future<Paginated<MyModel>> {
    return try MyModel.query(on: req).paginate(for: req)
}

Even return items off of the query builder

func test(_ req: Request) throws -> Future<Paginated<MyModel>> {
    return try MyModel.query(on: req).filter(\MyModel.name == "Test").paginate(for: req)
}

Making a request with the parameters is easy is appending ?page= and/or ?per=

curl "http://localhost:8080/api/v1/models?page=1&per=10"

A response looks like this

{
  "data": [{
    "updatedAt": "2018-03-07T00:00:00Z",
    "createdAt": "2018-03-07T00:00:00Z",
    "name": "My Test Model"
  }],
  "page": {
    "position": {
      "current": 1,
      "max": 1
    },
    "data": {
      "per": 10,
      "total": 2
    }
  }
}

Download Details:

Author: Vapor-community
Source Code: https://github.com/vapor-community/pagination 
License: MIT license

#swift #vapor 

Pagination: Simple Vapor 3 Pagination
Rupert  Beatty

Rupert Beatty

1672964040

MailCore: Emailing Wrapper for Vapor 3 Apps

MailCore

Mailing wrapper for multiple mailing services like Mailgun, SendGrid or SMTP

Features

  •  Mailgun
  •  SendGrid
  •  SMTP
  •  Attachments
  •  Multiple emails sent at the same time
  •  Multiple recipint, CC & BCC fields

Install

Just add following line package to your Package.swift file.

.package(url: "https://github.com/LiveUI/MailCore.git", .branch("master"))

Usage

Usage is really simple mkey!

1/3) Configure

First create your client configuration:

Mailgun

let config = Mailer.Config.mailgun(key: "{mailgunApi}", domain: "{mailgunDomain}", region: "{mailgunRegion}")

SendGrid

let config = Mailer.Config.sendGrid(key: "{sendGridApiKey}")

SMTP

Use the SMTP struct as a handle to your SMTP server:

let smtp = SMTP(hostname: "smtp.gmail.com",     // SMTP server address
                   email: "user@gmail.com",     // username to login
                password: "password")           // password to login
                
let config = Mailer.Config.smtp(smtp)

SMTP using TLS

All parameters of SMTP struct:

let smtp = SMTP(hostname: String,
                   email: String,
                password: String,
                    port: Int32 = 465,
                  useTLS: Bool = true,
        tlsConfiguration: TLSConfiguration? = nil,
             authMethods: [AuthMethod] = [],
             accessToken: String? = nil,
              domainName: String = "localhost",
                 timeout: UInt = 10)
                 
let config = Mailer.Config.smtp(smtp)

2/3) Register service

Register and configure the service in your apps configure method.

Mailer(config: config, registerOn: &services)

Mailer.Config is an enum and you can choose from any integrated services to be used

3/3) Send an email

let mail = Mailer.Message(from: "admin@liveui.io", to: "bobby.ewing@southfork.com", subject: "Oil spill", text: "Oooops I did it again", html: "<p>Oooops I did it again</p>")
return try req.mail.send(mail).flatMap(to: Response.self) { mailResult in
    print(mailResult)
    // ... Return your response for example
}

Testing

Mailcore provides a MailCoreTestTools framework which you can import into your tests to get MailerMock.

To register, and potentially override any existing "real" Mailer service, just initialize MailerMock with your services.

// Register
MailerMock(services: &services)

// Retrieve in your tests
let mailer = try! req.make(MailerService.self) as! MailerMock

MailerMock will store the last used result as well as the received message and request. Structure of the moct can be seen below:

public class MailerMock: MailerService {

    public var result: Mailer.Result = .success
    public var receivedMessage: Mailer.Message?
    public var receivedRequest: Request?

    // MARK: Initialization

    @discardableResult public init(services: inout Services) {
        services.remove(type: Mailer.self)
        services.register(self, as: MailerService.self)
    }

    // MARK: Public interface

    public func send(_ message: Mailer.Message, on req: Request) throws -> Future<Mailer.Result> {
        receivedMessage = message
        receivedRequest = req
        return req.eventLoop.newSucceededFuture(result: result)
    }

    public func clear() {
        result = .success
        receivedMessage = nil
        receivedRequest = nil
    }

}

Support

Join our Slack, channel #help-boost to ... well, get help :)

Enterprise AppStore

Core package for Einstore, a completely open source enterprise AppStore written in Swift!

Other core packages

  • EinstoreCore - AppStore core module
  • ApiCore - Base user & team management including forgotten passwords, etc ...

Implemented thirdparty providers

Code contributions

We love PR’s, we can’t get enough of them ... so if you have an interesting improvement, bug-fix or a new feature please don’t hesitate to get in touch. If you are not sure about something before you start the development you can always contact our dev and product team through our Slack.

Download Details:

Author: LiveUI
Source Code: https://github.com/LiveUI/MailCore 
License: MIT license

#swift #vapor #vapor 

MailCore: Emailing Wrapper for Vapor 3 Apps
Rupert  Beatty

Rupert Beatty

1672960080

Lingo-Vapor: Vapor Provider for Lingo - The Swift Localization Library

Lingo Provider

A Vapor provider for Lingo - a pure Swift localization library ready to be used in Server Side Swift projects.

Setup

Add a dependancy

Add LingoProvider as a dependancy in your Package.swift file:

dependencies: [
    ...,
    .package(name: "LingoVapor", url: "https://github.com/vapor-community/Lingo-Vapor.git", from: "4.2.0")]
],
targets: [
    .target(name: "App", dependencies: [
        .product(name: "LingoVapor", package: "Lingo-Vapor")

Upgrading from version 4.1.0 to version 4.2.0

The version 4.1.0 uses the new version of Lingo where the format of locale identifiers was changed to match RFC 5646. Prior to 4.2.0 _ was used to separate language code and country code in the locale identifier, and now the library uses - as per RFC.

If you were using any locales which include a country code, you would need to rename related translation files to match the new format.

Add the Provider

In the configure.swift simply initialize the LingoVapor with a default locale:

import LingoVapor
...
public func configure(_ app: Application) throws {
    ...
    app.lingoVapor.configuration = .init(defaultLocale: "en", localizationsDir: "Localizations")
}

The localizationsDir can be omitted, as the Localizations is also the default path. Note that this folder should exist under the workDir.

Use

After you have configured the provider, you can use lingoVapor service to create Lingo:

let lingo = try app.lingoVapor.lingo()
...
let localizedTitle = lingo.localize("welcome.title", locale: "en")

To get the locale of a user out of the request, you can use request.locale. This uses a language, which is in the HTTP header and which is in your available locales, if that exists. Otherwise it falls back to the default locale. Now you can use different locales dynamically:

let localizedTitle = lingo.localize("welcome.title", locale: request.locale)

When overwriting the requested locale, just write the new locale into the session, e.g. like that:

session.data["locale"] = locale

Use the following syntax for defining localizations in a JSON file:

{
    "title": "Hello Swift!",
    "greeting.message": "Hi %{full-name}!",
    "unread.messages": {
        "one": "You have one unread message.",
        "other": "You have %{count} unread messages."
    }
}

Locale redirection middleware

In case you want to serv different locales on different subfolders, you can use the LocaleRedirectMiddleware.

Add in configure.swift:

import LingoVapor

// Inside `configure(_ app: Application)`:
app.middleware.use(LocaleRedirectMiddleware())

Add in routes.swift:

import LingoVapor

// Inside `routes(_ app: Application)`:
app.get("home") { /* ... */ }
app.get(":locale", "home") { /* ... */ } // For each route, add the one prefixed by the `locale` parameter

That way, going to /home/ will redirect you to /<locale>/home/ (with <locale> corresponding to your browser locale), and going to /fr/home/ will display homepage in french whatever the browser locale is.

Inside Leaf templates

When using Leaf as templating engine, you can use LocalizeTag, LocaleTag and LocaleLinksTag from LingoVaporLeaf for localization inside the templates.

Add in configure.swift:

import LingoVaporLeaf

// Inside `configure(_ app: Application)`:
app.leaf.tags["localize"] = LocalizeTag()
app.leaf.tags["locale"] = LocaleTag()
app.leaf.tags["localeLinks"] = LocaleLinksTag()

Afterwards you can call them inside the Leaf templates:

<!-- String localization -->
#localize("thisisthelingokey")
#localize("lingokeywithvariable", "{\"foo\":\"bar\"}")

<!-- Get current locale -->
<html lang="#locale()">

<!-- Generate link canonical and alternate tags -->
#localeLinks("http://example.com/", "/canonical/path/")

Learn more

  • Lingo - learn more about the localization file format, pluralization support, and see how you can get the most out of the Lingo.

Download Details:

Author: Vapor-community
Source Code: https://github.com/vapor-community/Lingo-Vapor 
License: MIT license

#swift #localization #server #vapor 

Lingo-Vapor: Vapor Provider for Lingo - The Swift Localization Library
Rupert  Beatty

Rupert Beatty

1672956300

Leaf-markdown: Markdown Renderer for Vapor

Leaf Markdown

A Markdown renderer for Vapor and Leaf. This uses the Vapor Markdown package to wrap cmark (though a fork is used to make it work with Swift PM), so it understands Common Mark. A quick reference guide for Common Mark can be found here. It also supports Github Flavored Markdown.

Usage

Once set up, you can use it in your Leaf template files like any other tag:

#markdown(myMarkdown)

Where you have passed myMarkdown into the view as something like:

# Hey #

Check out my *awesome* markdown! It is easy to use in `tags`

Setup

Add as dependency

Add Leaf Markdown as a dependency in your Package.swift file:

    dependencies: [
        ...,
        .package(name: "LeafMarkdown", url: "https://github.com/vapor-community/leaf-markdown.git", .upToNextMajor(from: "3.0.0")),
    ]

Then add the dependency to your target:

.target(
    name: "App",
    dependencies: [
        // ...
        "LeafMarkdown"
    ],
    // ...
)

Register with Leaf

Register the tag with Leaf so Leaf knows about it:

app.leaf.tags["markdown"] = Markdown()

Don't forget to import LeafMarkdown in the file you register the tag with import LeafMarkdown.

Download Details:

Author: Vapor-community
Source Code: https://github.com/vapor-community/leaf-markdown 
License: MIT license

#swift #vapor #provider #markdown 

Leaf-markdown: Markdown Renderer for Vapor
Rupert  Beatty

Rupert Beatty

1672952400

Serve Up Custom 404 and Server Error Pages for Your Vapor App

Leaf Error Middleware is a piece of middleware for Vapor which allows you to return custom 404 and server error pages.

Note that this middleware is designed to be used for Leaf front-end websites only - it should not be used for providing JSON error responses for an API, for example.

Usage

First, add LeafErrorMiddleware as a dependency in your Package.swift file:

dependencies: [
    // ...,
    .package(url: "https://github.com/brokenhandsio/leaf-error-middleware.git", from: "4.0.0")
],
targets: [
    .target(
        name: "App", 
        dependencies: [
            .product(name: "Vapor", package: "vapor"),
            ..., 
            .product(name: "LeafErrorMiddleware", package: "leaf-error-middleware")
        ]
    ),
    // ...
]

Default Context

To use the LeafErrorMiddleware with the default context passed to templates, register the middleware service in configure.swift to your Application's middleware (make sure you import LeafErrorMiddleware at the top):

app.middleware.use(LeafErrorMiddlewareDefaultGenerator.build())

Make sure it appears before all other middleware to catch errors.

Custom Context

Leaf Error Middleware allows you to pass a closure to LeafErrorMiddleware to generate a custom context for the error middleware. This is useful if you want to be able to tell if a user is logged in on a 404 page for instance.

Register the middleware as follows:

let leafMiddleware = LeafErrorMiddleware() { status, error, req async throws -> SomeContext in
    SomeContext()
}
app.middleware.use(leafMiddleware)

The closure receives three parameters:

  • HTTPStatus - the status code of the response returned.
  • Error - the error caught to be handled.
  • Request - the request currently being handled. This can be used to log information, make external API calls or check the session.

Custom Mappings

By default, you need to include two Leaf templates in your application:

  • 404.leaf
  • serverError.leaf

However, you may elect to provide a dictionary mapping arbitrary error responses (i.e >= 400) to custom template names, like so:

let mappings: [HTTPStatus: String] = [
    .notFound: "404",
    .unauthorized: "401",
    .forbidden: "403"
]
let leafMiddleware = LeafErrorMiddleware(errorMappings: mappings) { status, error, req async throws -> SomeContext in
    SomeContext()
}

app.middleware.use(leafMiddleware)
// OR
app.middleware.use(LeafErrorMiddlewareDefaultGenerator.build(errorMappings: mapping))

By default, when Leaf Error Middleware catches a 404 error, it will return the 404.leaf template. This particular mapping also allows returning a 401.leaf or 403.leaf template based on the error. Any other error caught will return the serverError.leaf template. By providing a mapping, you override the default 404 template and will need to respecify it if you want to use it.

Default Context

If using the default context, the serverError.leaf template will be passed up to three parameters in its context:

  • status - the status code of the error caught
  • statusMessage - a reason for the status code
  • reason - the reason for the error, if known. Otherwise this won't be passed in.

The 404.leaf template and any other custom error templates will get a reason parameter in the context if one is known.

Download Details:

Author: Brokenhandsio
Source Code: https://github.com/brokenhandsio/leaf-error-middleware 
License: MIT license

#swift #vapor 

Serve Up Custom 404 and Server Error Pages for Your Vapor App