WIP work on macro-based ORM layer.
This commit is contained in:
parent
84b82f659a
commit
61849d5e35
@ -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}
|
||||||
|
Binary file not shown.
@ -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
|
||||||
]#
|
]#
|
||||||
|
35
api/src/main/nim/personal_measure_apipkg/models.nim
Normal file
35
api/src/main/nim/personal_measure_apipkg/models.nim
Normal 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
|
@ -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" (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user