[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.
	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, Preparation and Model protocols:

final class Post: NodeInitializable {
  
    let storage = Storage()
    
	// Indicates if the object was retrieved from the database, or created.
	// Do not modify it directly.
	var exists = false

	// [...]
    
    init(row: Row) throws {
		title = try row.get("title")
		body = try row.get("body")
		rawBody = try row.get("rawbody")
		truncatedBody = try row.get("truncatedbody")
		datetime = try row.get("datetime")
		date = try row.get("date")
		modified = try row.get("modified")
		link = try row.get("link")
		readingTime = try row.get("readingtime")
	}
    
    init(node: Node) throws {
		title = try node.get("title")
		body = try node.get("body")
		rawBody = try node.get("rawbody")
		datetime = try node.get("datetime")
		modified = try node.get("modified")
		link = try node.get("link")
		truncatedBody = try node.get("truncatedbody")
		readingTime = try node.get("readingtime")
		date = try node.get("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 {
  
  func makeRow() throws -> Row {
		var row = Row()
		
		try row.set("title", title)
		try row.set("body", body)
		try row.set("rawbody", rawBody)
		try row.set("truncatedbody", truncatedBody)
		try row.set("datetime", datetime)
		try row.set("date", date)
		try row.set("modified", modified)
		try row.set("link", link)
		try row.set("readingtime", readingTime)
		
		return row
	}
  
}

extension Post: Preparation {

	// 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(self) { 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(self)
	}

}

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.makeQuery()
	.sort("datetime", .descending)
	.sort("title", .ascending)

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