Description
Hi all, I've been using better-sqlite3 for a few years on small-ish projects and libraries. In those instances, using the library doesn't require a lot of thought. It generally looks like:
- instantiate a database
- instantiate some statements
- start the application
However I want to take a stab at a large-ish web application using a sqlite backend. I find I'm bikeshedding a ton on the proper way to structure this project. The questions I find myself asking are:
- How should I go about typing (in typescript) my queries?
- Should I use model files?
- How do I handle migrations?
- How do I handle preparing statements that depend on migrations being ran?
The current iteration looks like the following:
- initialize better-sqlite3 database
- grab project version from a table that I know will exist for the project's lifetime
- if the current version > version in db, perform each migration in between
- initialize all the other statements
A model file essentially looks like this:
/* --============= Table Row Definitions =============-- */
interface BookTR {
id: number
title: string
published: boolean
published_date: Date
}
/* --================ Model Definition ================-- */
class BookModel extends Model {
private insert = this.register(InsertBookStatement)
private select_one = this.register(SelectOneBookStatement)
public create(book_data: Omit<BookTR, 'id'>) {
const book_id = this.insert(book_data)
return this.select_one(book_id)
}
}
/* --=================== Statements ===================-- */
class InsertBookStatement extends Statement {
sql = `INSERT INTO book (title, published, published_date) VALUES (@title, @published, @published_date)`
stmt = this.register(this.sql)
call(book_data: Omit<BookTR, 'id'>) {
const sql_data = {...book_data, published ? 1 : 0, published_date: book_data.published_date.toString()}
const info = this.stmt.ref.run(sql_data)
return info.lastInsertRowid
}
}
class SelectOneBookStatement extends Statement {
sql = `SELECT * FROM book WHERE id = ?`
stmt = this.register(this.sql)
call(book_id: BookTR['id']) {
return this.stmt.ref.get(book_id)
}
}
and the main database file looks like this:
class Database {
constructor(database_path: string) {
this.db = new BetterSqlite3(database_path)
}
init() {
for (const model of this.registered_models) model.init()
}
// model definitions
book = this.register(BookModel)
}
usage:
const db = new Database('sqlite.db')
db.init()
const book = db.book.create({ title: 'Oh The Places You'll Go', published: true, published_date: new Date('5/24/1997') })
essentially all those register
methods do on Statements, Models and Database is instantiate the class with a reference to the better-sqlite3 database, and store registered objects in an array that is iterated on in an init
method (what this really allows for is preparing all sql statements when the init method is called).