WIP work on macro-based ORM layer.

This commit is contained in:
Jonathan Bernard 2019-02-15 14:23:44 -06:00
parent 84b82f659a
commit 61849d5e35
5 changed files with 145 additions and 82 deletions

View File

@ -22,4 +22,4 @@ delete-postgres-container:
rm postgres.container.id rm postgres.container.id
connect: connect:
PGPASSWORD=password psql -p 5500 -U postgres -h localhost PGPASSWORD=password psql -p 5500 -U postgres -h localhost ${DB_NAME}

View File

@ -1,30 +1,11 @@
import db_postgres, macros, options, postgres, sequtils, strutils, times, timeutils, uuids import db_postgres, macros, options, postgres, sequtils, strutils, times, timeutils, uuids
import ./models
import nre except toSeq import nre except toSeq
from unicode import capitalize, toLower from unicode import capitalize, toLower
type type
User* = object
id*: UUID
displayName*, email*, hashedPwd*: string
ApiToken* = object
id*, userId: UUID
name*, hashedToken: string
expires: Option[DateTime]
Measure* = object
id*, userId*: UUID
slug*, name*, description*, domainUnits*, rangeUnits*: string
domainSource*, rangeSource*: Option[string]
analysis*: seq[string]
Value* = object
id*, measureId*: UUID
value: int
timestamp: DateTime
extData: string
MutateClauses = object MutateClauses = object
columns*: seq[string] columns*: seq[string]
placeholders*: seq[string] placeholders*: seq[string]
@ -47,66 +28,108 @@ proc dbFormat[T](list: seq[T]): string =
proc dbFormat[T](item: T): string = return $item proc dbFormat[T](item: T): string = return $item
macro populateMutateClauses(t: typed, newRecord: bool, mc: var MutateClauses): untyped = proc createParseStmts(t: NimNode, value: string): NimNode =
result = newStmtList()
# Must be working with an object. if t.typeKind == ntyObject:
if t.getType == UUID.getType:
result.add quote do:
discard parseUUID(`value`)
elif t.getType == DateTime.getType:
result.add quote do:
discard `value`.parseIso8601
elif t.typeKind == ntyString:
result.add quote do:
discard `value`
template walkFieldDefs(t: NimNode, body: untyped) =
let tTypeImpl = t.getTypeImpl let tTypeImpl = t.getTypeImpl
if not (tTypeImpl.typeKind == ntyObject):
error $t & " is not an object." var nodeToItr: NimNode
if tTypeImpl.typeKind == ntyObject: nodeToItr = tTypeImpl[2]
elif tTypeImpl.typeKind == ntyTypeDesc: nodeToItr = tTypeImpl.getType[1].getType[2]
else: error $t & " is not an object or type desc (it's a " & $tTypeImpl.typeKind & ")."
for fieldDef {.inject.} in nodeToItr.children:
# ignore AST nodes that are not field definitions
if fieldDef.kind == nnkIdentDefs:
let fieldIdent {.inject.} = fieldDef[0]
let fieldType {.inject.} = fieldDef[1]
body
elif fieldDef.kind == nnkSym:
let fieldIdent {.inject.} = fieldDef
let fieldType {.inject.} = fieldDef.getType
body
macro populateMutateClauses(t: typed, newRecord: bool, mc: var MutateClauses): untyped =
result = newStmtList() result = newStmtList()
# iterate over all the object's fields # iterate over all the object's fields
for child in tTypeImpl[2].children: t.walkFieldDefs:
# ignore AST nodes that are not field definitions # grab the field, it's string name, and it's type
if child.kind == nnkIdentDefs: let fieldName = $fieldIdent
# grab the field, it's string name, and it's type # we do not update the ID, but we do check: if we're creating a new
let field = child[0] # record, we should not have an existing ID
let fieldType = child[1] if fieldName == "id":
let fieldName = $field result.add quote do:
if `newRecord` and not `t`.id.isZero:
raise newException(
AssertionError,
"Trying to create a new record, but the record already has an ID.")
# we do not update the ID, but we do check: if we're creating a new # if we're looking at an optional field, add logic to check for presence
# record, we should not have an existing ID elif fieldType.kind == nnkBracketExpr and
if fieldName == "id": fieldType.len > 0 and
result.add quote do: fieldType[0] == Option.getType:
if `newRecord` and not `t`.id.isZero:
raise newException(
AssertionError,
"Trying to create a new record, but the record already has an ID.")
# if we're looking at an optional field, add logic to check for presence result.add quote do:
elif fieldType.kind == nnkBracketExpr and `mc`.columns.add(identNameToDb(`fieldName`))
fieldType.len > 0 and if `t`.`fieldIdent`.isSome:
fieldType[0] == Option.getType:
result.add quote do:
`mc`.columns.add(`fieldName`)
if `t`.`field`.isSome:
`mc`.placeholders.add("?")
`mc`.values.add(dbFormat(`t`.`field`.get))
else:
`mc`.placeholders.add("NULL")
# otherwise assume we can convert and go ahead.
else:
result.add quote do:
`mc`.columns.add(`fieldName`)
`mc`.placeholders.add("?") `mc`.placeholders.add("?")
`mc`.values.add(dbFormat(`t`.`field`)) `mc`.values.add(dbFormat(`t`.`fieldIdent`.get))
else:
`mc`.placeholders.add("NULL")
# otherwise assume we can convert and go ahead.
else:
result.add quote do:
`mc`.columns.add(identNameToDb(`fieldName`))
`mc`.placeholders.add("?")
`mc`.values.add(dbFormat(`t`.`fieldIdent`))
#echo result.repr #echo result.repr
# TODO # TODO
#macro rowToRecord(recType: untyped, row: seq[string]): untyped macro rowToModel(modelType: typed): untyped =
# echo recType result = newStmtList()
macro recordName(rec: typed): string = #echo modelType.getType[1].getType.treeRepr
return $rec.getTypeInst modelType.walkFieldDefs:
result.add createParseStmts(fieldType, "")
#[
result.add quote do:
User(
id: genUUID
#modelType.walkFieldDefs:
]#
proc tableNameForRecord[T](rec: T): string = macro modelName(model: typed): string =
return recordName(rec).replace(UPPERCASE_PATTERN, "$1_$2").toLower() & "s" return $model.getTypeInst
proc identNameToDb(name: string): string =
return name.replace(UPPERCASE_PATTERN, "$1_$2").toLower()
proc dbNameToIdent(name: string): string =
let parts = name.split("_")
return @[parts[0]].concat(parts[1..^1].mapIt(capitalize(it))).join("")
proc tableName[T](rec: T): string =
return modelName(rec).identNameToDb & "s"
proc createRecord[T](db: DbConn, rec: T): T = proc createRecord[T](db: DbConn, rec: T): T =
var mc = newMutateClauses() var mc = newMutateClauses()
@ -115,7 +138,7 @@ proc createRecord[T](db: DbConn, rec: T): T =
# Confusingly, getRow allows inserts and updates. We use it to get back the ID # Confusingly, getRow allows inserts and updates. We use it to get back the ID
# we want from the row. # we want from the row.
let newIdStr = db.getValue(sql( let newIdStr = db.getValue(sql(
"INSERT INTO " & tableNameForRecord(rec) & "INSERT INTO " & tableName(rec) &
" (" & mc.columns.join(",") & ") " & " (" & mc.columns.join(",") & ") " &
" VALUES (" & mc.placeholders.join(",") & ") " & " VALUES (" & mc.placeholders.join(",") & ") " &
" RETURNING id"), mc.values) " RETURNING id"), mc.values)
@ -129,7 +152,7 @@ proc updateRecord[T](db: DbConn, rec: T): bool =
let setClause = zip(mc.columns, mc.placeholders).mapIt(it.a & " = " it.b).join(',') let setClause = zip(mc.columns, mc.placeholders).mapIt(it.a & " = " it.b).join(',')
let numRowsUpdated = db.execAffectedRows(sql( let numRowsUpdated = db.execAffectedRows(sql(
"UPDATE " & tableNameForRecord(rec) & "UPDATE " & tableName(rec) &
" SET " & setClause & " SET " & setClause &
" WHERE id = ? "), mc.values.concat(@[rec.id])) " WHERE id = ? "), mc.values.concat(@[rec.id]))
@ -138,33 +161,38 @@ proc updateRecord[T](db: DbConn, rec: T): bool =
# TODO # TODO
#proc getRecord[T](db: DbConn, UUID id): T = #proc getRecord[T](db: DbConn, UUID id): T =
macro listFieldNames(t: typed): untyped = macro listFields(t: typed): untyped =
let tTypeImpl = t.getTypeImpl var fields: seq[tuple[n: string, t: string]] = @[]
t.walkFieldDefs:
if fieldDef.kind == nnkSym: fields.add((n: $fieldIdent, t: fieldType.repr))
else: fields.add((n: $fieldIdent, t: $fieldType))
if not (tTypeImpl.typeKind == ntyObject): result = newLit(fields)
error $t & " is not an object."
var fieldNames: seq[tuple[n: string, t: string]] = @[]
for child in tTypeImpl[2].children:
if child.kind == nnkIdentDefs:
echo $child[1]
fieldNames.add((n: $child[0], t: $child[1]))
result = newLit(fieldNames)
# proc create: Typed create methods for specific records
proc createUser*(db: DbConn, user: User): User = db.createRecord(user) proc createUser*(db: DbConn, user: User): User = db.createRecord(user)
proc createApiToken*(db: DbConn, token: ApiToken): ApiToken = db.createRecord(token) proc createApiToken*(db: DbConn, token: ApiToken): ApiToken = db.createRecord(token)
proc createMeasure*(db: DbConn, measure: Measure): Measure = db.createRecord(measure) proc createMeasure*(db: DbConn, measure: Measure): Measure = db.createRecord(measure)
proc createValue*(db: DbConn, value: Value): Value = db.createRecord(value) proc createValue*(db: DbConn, value: Value): Value = db.createRecord(value)
#[
when isMainModule: when isMainModule:
rowToModel(User)
let u = User( let u = User(
displayName: "Bob", displayName: "Bob",
email: "bob@bobsco.com", email: "bob@bobsco.com",
hashedPwd: "test") hashedPwd: "test")
echo createRecord(nil, u) #[
let db = open("", "", "", "host=localhost port=5500 dbname=personal_measure user=postgres password=password")
for row in db.fastRows(sql"SELECT * FROM users"):
echo $row
echo "----"
rowToModel(User)
echo "New user:\n\t" & $db.createUser(u)
for row in db.fastRows(sql"SELECT * FROM users"):
echo $row
]# ]#

View File

@ -0,0 +1,35 @@
import options, times, uuids
type
User* = object
id*: UUID
displayName*, email*, hashedPwd*: string
ApiToken* = object
id*, userId*: UUID
name*, hashedToken*: string
expires*: Option[DateTime]
Measure* = object
id*, userId*: UUID
slug*, name*, description*, domainUnits*, rangeUnits*: string
domainSource*, rangeSource*: Option[string]
analysis*: seq[string]
Value* = object
id*, measureId*: UUID
value*: int
timestamp*: DateTime
extData*: string
proc `$`*(u: User): string =
return "User " & ($u.id)[0..6] & " - " & u.displayName & " <" & u.email & ">"
proc `$`*(tok: ApiToken): string =
return "ApiToken " & ($tok.id)[0..6] & " - " & tok.name
proc `$`*(m: Measure): string =
return "Measure " & ($m.id)[0..6] & " - " & m.slug
proc `$`*(v: Value): string =
return "Value " & ($v.id)[0..6] & " - " & ($v.measureId)[0..6] & " = " & $v.value

View File

@ -5,7 +5,7 @@ create table "users" (
id uuid default uuid_generate_v4() primary key, id uuid default uuid_generate_v4() primary key,
display_name varchar not null, display_name varchar not null,
email varchar not null, email varchar not null,
hashedpwd varchar not null hashed_pwd varchar not null
); );
create table "api_tokens" ( create table "api_tokens" (