1673010960
Mailgun
is a Vapor 4 service for a popular email sending API
Note: Vapor3 version is available in
vapor3
branch and from3.0.0
tag
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")
])
Make sure you get an API key and register a custom domain
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
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
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(...)
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 withApplication
as in example above.
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)
}
}
Author: Vapor-community
Source Code: https://github.com/vapor-community/mailgun
1673007060
Adds the powerful logging of SwiftyBeaver to Vapor for server-side Swift 4 on Linux and Mac.
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")
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,
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)
}
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)
}
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:
KEY | AVAILABLE FOR | TYPE | OBSERVATION |
---|---|---|---|
async | console, file | Bool | |
format | console, file | String | A space must be placed before dollar sign |
levelString.debug | console, file | String | |
levelString.error | console, file | String | |
levelString.info | console, file | String | |
levelString.verbose | console, file | String | |
levelString.warning | console, file | String | |
path | file | String | path to the log file |
minLevel | console, file, platform | String | values: verbose, debug, info, warning, error |
threshold | platform | Int | min: 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
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.
Learn more about colored logging to Xcode 8 Console.
Learn more about logging to file which is great for Terminal.app fans or to store logs on disk.
Learn more about logging to the SwiftyBeaver Platform during release!
Get support via Github Issues, email and our public Slack channel.
This package is developed and maintained by Gustavo Perdomo with the collaboration of all vapor community.
Author: Vapor-community
Source Code: https://github.com/vapor-community/swiftybeaver-provider
License: MIT license
1673003040
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" ...]
),
...
]
Make sure that you've imported Sugar everywhere when needed:
import Sugar
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:
Access environment variables by writing
env("my-key", "my-fallback-value")
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.
This package contains a lot of convenience related to JWT, usernames and passwords which is used in JWTKeychain and Admin Panel.
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))
}
}
}
This package is developed and maintained by the Vapor team at Nodes.
Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/sugar
License: MIT license
1672999200
Installation
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")
]
),
...
]
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 Field
s 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 Field
s.
First make sure that you've imported Submissions everywhere it's needed:
import Submissions
"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.
TODO
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 Field
s 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.
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:
Position | Type | Description | Example | Required? |
---|---|---|---|---|
1 | key | Key to the related field in the field cache | "name" | yes |
2 | placeholder | Placeholder text | "Enter name" | no |
3 | help text | Help text | "This name will be visible to others" | no |
To add a file upload to your form use this leaf tag.
#submissions:file( ... )
With these options:
Position | Type | Description | Example | Required? |
---|---|---|---|---|
1 | key | Key to the related field in the field cache | "avatar" | yes |
2 | help text | Help text | "This will replace your existing avatar" | no |
3 | accept | Placeholder text | "image/*" | no |
4 | multiple | Support multple file uploads | "true" (or any other non-nil value) | no |
A select tag can be added as follows.
#submissions:select( ... )
With these options:
Position | Type | Description | Example | Required? |
---|---|---|---|---|
1 | key | Key to the related field in the field cache | "role" | yes |
2 | options | The possible options in the drop down | roles | no |
3 | placeholder | Placeholder text | "Select an role" | no |
4 | help text | Help 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)
This package is developed and maintained by the Vapor team at Nodes.
Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/submissions
License: MIT license
1672995060
A package to ease the use of multiple storage and CDN services.
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" ...]
),
...
]
Storage makes it easy to start uploading and downloading files. Just register a network driver and get going.
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
)
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)
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)
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)
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/")
}
Deleting a file can be done as follows.
try Storage.delete("/images/profile.png")
Storage
has a variety of configurable options.
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
, accessKey
and 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
.
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
This package is developed and maintained by the Vapor team at Nodes.
Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/storage
License: MIT license
1672991220
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")
import Slugify
print("My test URL æøΓ₯".slugify())
The above code will print: my-test-url-aeoa
This package is developed and maintained by the Vapor team at Nodes. The package owner for this project is John.
Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/slugify
License: MIT license
1672987440
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:
Linux | macOS |
---|---|
/var/log/Vapor/ | ~/Library/Caches/Vapor/ |
You can change Vapor/
to an arbitrary directory by changing the executableName
during setup.
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.
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)
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)
}
Author: Hallee
Source Code: https://github.com/hallee/vapor-simple-file-logger
License: MIT license
1672983480
Adds a mail backend for SendGrid to the Vapor web framework. Send simple emails, or leverage the full capabilities of SendGrid's V3 API.
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
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)
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)")
}
// ...
}
Author: Vapor-community
Source Code: https://github.com/vapor-community/sendgrid
License: MIT license
1672979700
Powerful model extraction from JSON requests.
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")
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
{
"id": 1,
"name": "John Appleseed",
"email": "example@domain.com"
}
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
}
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."
)
}
}
}
This package is developed and maintained by Gustavo Perdomo.
This package is heavily inspired by Sanitized
Author: Gperdomor
Source Code: https://github.com/gperdomor/sanitize
License: MIT license
1672971960
This package currently offers support for offset pagination on Array
and QueryBuilder
.
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.
First make sure that you've imported Paginator everywhere it's needed:
import Paginator
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
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 therender
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>
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()
}
}
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
))
}
This package is developed and maintained by the Vapor team at Nodes.
Author: Nodes-vapor
Source Code: https://github.com/nodes-vapor/paginator
License: MIT license
1672968180
Pagination is based off of the Fluent 2 pagination system.
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
}
}
}
Author: Vapor-community
Source Code: https://github.com/vapor-community/pagination
License: MIT license
1672964040
Mailing wrapper for multiple mailing services like Mailgun, SendGrid or SMTP
Features
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!
First create your client configuration:
let config = Mailer.Config.mailgun(key: "{mailgunApi}", domain: "{mailgunDomain}", region: "{mailgunRegion}")
let config = Mailer.Config.sendGrid(key: "{sendGridApiKey}")
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)
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)
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
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
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.
Author: LiveUI
Source Code: https://github.com/LiveUI/MailCore
License: MIT license
1672960080
A Vapor provider for Lingo - a pure Swift localization library ready to be used in Server Side Swift projects.
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")
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.
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.
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."
}
}
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.
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/")
Author: Vapor-community
Source Code: https://github.com/vapor-community/Lingo-Vapor
License: MIT license
1672956300
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.
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`
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 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
.
Author: Vapor-community
Source Code: https://github.com/vapor-community/leaf-markdown
License: MIT license
1672952400
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")
]
),
// ...
]
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.
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.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.
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 caughtstatusMessage
- a reason for the status codereason
- 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.
Author: Brokenhandsio
Source Code: https://github.com/brokenhandsio/leaf-error-middleware
License: MIT license