Initial stab at better documentation.

This commit is contained in:
2022-09-02 23:25:52 -05:00
parent 1d7c955805
commit 9bf3c4f3ec
6 changed files with 223 additions and 9 deletions

View File

@ -1,3 +1,38 @@
# Fiber ORM
#
# Copyright 2019 Jonathan Bernard <jonathan@jdbernard.com>
## Lightweight ORM supporting the `Postgres`_ and `SQLite`_ databases in Nim.
## It supports a simple, opinionated model mapper to generate SQL queries based
## on Nim objects. It also includes a simple connection pooling implementation.
##
## .. _Postgres: https://nim-lang.org/docs/db_postgres.html
## .. _SQLite: https://nim-lang.org/docs/db_sqlite.html
##
## Basic Usage
## ===========
##
## Object-Relational Modeling
## ==========================
##
## Model Class
## -----------
##
## Table Name
## ``````````
##
## Column Names
## ````````````
##
## ID Field
## ````````
##
## Supported Data Types
## --------------------
##
## Database Object
## ===============
import std/db_postgres, std/macros, std/options, std/sequtils, std/strutils
import namespaced_logging, uuids
@ -16,7 +51,8 @@ export
util.rowToModel,
util.tableName
type NotFoundError* = object of CatchableError
type NotFoundError* = object of CatchableError ##\
## Error type raised when no record matches a given ID
var logNs {.threadvar.}: LoggingNamespace
@ -31,6 +67,15 @@ proc newMutateClauses(): MutateClauses =
values: @[])
proc createRecord*[T](db: DbConn, rec: T): T =
## Create a new record. `rec` is expected to be a `model class`_. The `id
## field`_ is only set if it is `non-empty`_
##
## Returns the newly created record.
##
## .. _model class: #objectminusrelational-modeling-model-class
## .. _id field: #model-class-id-field
## .. _non-empty:
var mc = newMutateClauses()
populateMutateClauses(rec, true, mc)
@ -46,6 +91,9 @@ proc createRecord*[T](db: DbConn, rec: T): T =
result = rowToModel(T, newRow)
proc updateRecord*[T](db: DbConn, rec: T): bool =
## Update a record by id. `rec` is expected to be a `model class`_.
##
## .. _model class: #objectminusrelational-modeling-model-class
var mc = newMutateClauses()
populateMutateClauses(rec, false, mc)
@ -61,16 +109,21 @@ proc updateRecord*[T](db: DbConn, rec: T): bool =
return numRowsUpdated > 0;
template deleteRecord*(db: DbConn, modelType: type, id: typed): untyped =
## Delete a record by id.
let sqlStmt = "DELETE FROM " & tableName(modelType) & " WHERE id = ?"
log().debug "deleteRecord: [" & sqlStmt & "] id: " & $id
db.tryExec(sql(sqlStmt), $id)
proc deleteRecord*[T](db: DbConn, rec: T): bool =
## Delete a record by `id`_.
##
## .. _id: #model-class-id-field
let sqlStmt = "DELETE FROM " & tableName(rec) & " WHERE id = ?"
log().debug "deleteRecord: [" & sqlStmt & "] id: " & $rec.id
return db.tryExec(sql(sqlStmt), $rec.id)
template getRecord*(db: DbConn, modelType: type, id: typed): untyped =
## Fetch a record by id.
let sqlStmt =
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType) &
@ -85,6 +138,9 @@ template getRecord*(db: DbConn, modelType: type, id: typed): untyped =
rowToModel(modelType, row)
template findRecordsWhere*(db: DbConn, modelType: type, whereClause: string, values: varargs[string, dbFormat]): untyped =
## Find all records matching a given `WHERE` clause. The number of elements in
## the `values` array must match the number of placeholders (`?`) in the
## provided `WHERE` clause.
let sqlStmt =
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType) &
@ -94,6 +150,7 @@ template findRecordsWhere*(db: DbConn, modelType: type, whereClause: string, val
db.getAllRows(sql(sqlStmt), values).mapIt(rowToModel(modelType, it))
template getAllRecords*(db: DbConn, modelType: type): untyped =
## Fetch all records of the given type.
let sqlStmt =
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType)
@ -102,6 +159,7 @@ template getAllRecords*(db: DbConn, modelType: type): untyped =
db.getAllRows(sql(sqlStmt)).mapIt(rowToModel(modelType, it))
template findRecordsBy*(db: DbConn, modelType: type, lookups: seq[tuple[field: string, value: string]]): untyped =
## Find all records matching the provided lookup values.
let sqlStmt =
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType) &
@ -112,6 +170,23 @@ template findRecordsBy*(db: DbConn, modelType: type, lookups: seq[tuple[field: s
db.getAllRows(sql(sqlStmt), values).mapIt(rowToModel(modelType, it))
macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untyped =
## Generate all standard access procedures for the given model types. For a
## `model class`_ named `SampleRecord`, this will generate the following
## procedures:
##
## .. code-block:: Nim
## proc getSampleRecord*(db: dbType): SampleRecord;
## proc getAllSampleRecords*(db: dbType): SampleRecord;
## proc createSampleRecord*(db: dbType, rec: SampleRecord): SampleRecord;
## proc deleteSampleRecord*(db: dbType, rec: SampleRecord): bool;
## proc deleteSampleRecord*(db: dbType, id: idType): bool;
## proc updateSampleRecord*(db: dbType, rec: SampleRecord): bool;
##
## proc findSampleRecordsWhere*(
## db: dbType, whereClause: string, values: varargs[string]): SampleRecord;
##
## `dbType` is expected to be some type that has a defined `withConn`_
## procedure.
result = newStmtList()
for t in modelTypes:
@ -147,6 +222,15 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
db.withConn: result = deleteRecord(conn, `t`, id)
macro generateLookup*(dbType: type, modelType: type, fields: seq[string]): untyped =
## Create a lookup procedure for a given set of field names. For example,
##
## .. code-block:: Nim
## generateLookup(SampleDB, SampleRecord, ["name", "location"])
##
## will generate the following procedure:
##
## .. code-block:: Nim
## proc findSampleRecordsByNameAndLocation*(db: SampleDB,
let fieldNames = fields[1].mapIt($it)
let procName = ident("find" & pluralize($modelType.getType[1]) & "By" & fieldNames.mapIt(it.capitalize).join("And"))
@ -211,6 +295,20 @@ proc initPool*(
poolSize = 10,
hardCap = false,
healthCheckQuery = "SELECT 'true' AS alive"): DbConnPool =
## Initialize a new DbConnPool.
##
## * `connect` must be a factory which creates a new `DbConn`
## * `poolSize` sets the desired capacity of the connection pool.
## * `hardCap` defaults to `false`.
##
## When `false`, the pool can grow beyond the configured capacity, but will
## release connections down to the its capacity (no less than `poolSize`).
##
## When `true` the pool will not create more than its configured capacity.
## It a connection is requested, none are free, and the pool is at
## capacity, this will result in an Error being raised.
## * `healthCheckQuery` should be a simple and fast SQL query that the pool
## can use to test the liveliness of pooled connections.
initDbConnPool(DbConnPoolConfig(
connect: connect,