[SSS] PostgreSQL models

:  ~ 3 min read

Let's start by defining our Post model:

struct Post {

	let title: String
	var rawBody: String { // The original, markdown body.
		didSet {
			// For updating body, truncatedBody and readingTime automatically.
			// didSet doesn't get called on init too, sadly.
		}
	}
	fileprivate(set) var body: String // The html body.
	fileprivate(set) var truncatedBody: String // The html body, truncated to x chars.
	fileprivate(set) var readingTime: String
	let datetime: String // The date, in yyyy-MM-dd-HHmm format.
	let link: String // The link, created from the title, in post-title format.
	let date: String // The short date, to be used as subtitle.
	var modified: String // The last modified date, in datetime format.

}

I'm not going into details on how body truncation, Markdown -> HTML conversion and reading time work, as they can be found here, and they are pretty nicely documented.

Next, making Post database compliant: Vapor contains a framework called Fluent, an ORM tool for a handful of database providers, and we'll use it for talking with PostgreSQL. We have to conform to the NodeInitializable, NodeRepresentable and Model protocols:

struct Post {

	// Will be nil until it is saved in the database.
	var id: Node?
	// Indicates if the object was retrieved from the database, or created.
	// Do not modify it directly.
	var exists = false

	// [...]

}

// Has to be in the same file, due to the fileprivate(set) properties.
extension Post: NodeInitializable {

	// Helps Fluent to initialize a Post from the database.
	// These will be all lowercase, because PostgreSQL column names are all lowercase.
	init(node: Node, in context: Context) throws {
		id = try node.extract("id")
		title = try node.extract("title")
		body = try node.extract("body")
		rawBody = try node.extract("rawbody")
		datetime = try node.extract("datetime")
		modified = try node.extract("modified")
		link = try node.extract("link")
		truncatedBody = try node.extract("truncatedbody")
		readingTime = try node.extract("readingtime")
		date = try node.extract("date")
	}

}

// Will be in the same file as well, just because it's really short.
extension Post: NodeRepresentable {

	// Helps Fluent save a Post to the database.
	func makeNode(context: Context) throws -> Node {
		return try Node(node: [
			"id": id,
			"title": title,
			"body": body,
			"rawBody": rawBody,
			"truncatedBody": truncatedBody,
			"datetime": datetime,
			"link": link,
			"readingTime": readingTime,
			"date": date,
			"modified": modified]
		)
	}

}

// Post+Model.swift:

extension Post: Model {

	// This tells Fluent how the table schema should look like.
	// These will be all lowercase, because PostgreSQL column names are all lowercase.
	static func prepare(_ database: Database) throws {
		try database.create("posts") { posts in
			posts.id()
			posts.string("title", length: 9_999, optional: false, unique: false, default: nil)
			posts.string("body", length: 999_999, optional: false, unique: false, default: nil)
			posts.string("rawbody", length: 999_999, optional: false, unique: false, default: nil)
			posts.string("truncatedbody", length: 1200, optional: false, unique: false, default: nil)
			posts.string("datetime", length: 15, optional: false, unique: false, default: nil)
			posts.string("date", length: 12, optional: false, unique: false, default: nil)
			posts.string("modified", length: 15, optional: false, unique: false, default: nil)
			posts.string("link", length: 100, optional: false, unique: true, default: nil)
			posts.string("readingtime", length: 15, optional: false, unique: false, default: nil)
		}
	}

	// This tells Fluent what reverting means, in our case it will just drop the table.
	static func revert(_ database: Database) throws {
		try database.delete("posts")
	}

}

Configuring PostgreSQL is a matter of filling the postgresql.json file, located in Config/secrets:

{
	"host": "127.0.0.1",
	"user": "roland", // Usually your Mac's username.
	"password": "",
	"database": "roland", // Usually your Mac's username.
	"port": 5432
}

Now, making use of PostgreSQL is as easy as:

// Saving:

// Has to be a variable, because the save() method is mutating - it updates the id field.
var post = Post(title: "Test title", rawBody: "Test body", datetime: "2017-03-09-1550")
try post.save()

// Fetching:

let posts = try Post.all()
let firstPost = posts.first
try firstPost?.delete()

// More complex fetching:

let sortedPosts = try Post.query()
	.sort("datetime", .descending)
	.sort("title", .ascending)

sortedPosts.forEach { print($0.datetime + ": " + $0.title) }