diff --git a/api/Makefile b/api/Makefile index 0fae820..d09f47b 100644 --- a/api/Makefile +++ b/api/Makefile @@ -22,4 +22,4 @@ delete-postgres-container: rm postgres.container.id connect: - PGPASSWORD=password psql -p 5500 -U postgres -h localhost + PGPASSWORD=password psql -p 5500 -U postgres -h localhost ${DB_NAME} diff --git a/api/src/main/nim/personal_measure_apipkg/db b/api/src/main/nim/personal_measure_apipkg/db deleted file mode 100755 index 10a7594..0000000 Binary files a/api/src/main/nim/personal_measure_apipkg/db and /dev/null differ diff --git a/api/src/main/nim/personal_measure_apipkg/db.nim b/api/src/main/nim/personal_measure_apipkg/db.nim index 763b1d3..cd90d9e 100644 --- a/api/src/main/nim/personal_measure_apipkg/db.nim +++ b/api/src/main/nim/personal_measure_apipkg/db.nim @@ -1,30 +1,11 @@ import db_postgres, macros, options, postgres, sequtils, strutils, times, timeutils, uuids +import ./models + import nre except toSeq from unicode import capitalize, toLower 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 columns*: seq[string] placeholders*: seq[string] @@ -47,66 +28,108 @@ proc dbFormat[T](list: seq[T]): string = 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 - 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() # iterate over all the object's fields - for child in tTypeImpl[2].children: + t.walkFieldDefs: - # ignore AST nodes that are not field definitions - if child.kind == nnkIdentDefs: + # grab the field, it's string name, and it's type + let fieldName = $fieldIdent - # grab the field, it's string name, and it's type - let field = child[0] - let fieldType = child[1] - let fieldName = $field + # we do not update the ID, but we do check: if we're creating a new + # record, we should not have an existing ID + if fieldName == "id": + 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 - # record, we should not have an existing ID - if fieldName == "id": - 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.") + # if we're looking at an optional field, add logic to check for presence + elif fieldType.kind == nnkBracketExpr and + fieldType.len > 0 and + fieldType[0] == Option.getType: - # if we're looking at an optional field, add logic to check for presence - elif fieldType.kind == nnkBracketExpr and - fieldType.len > 0 and - 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`) + result.add quote do: + `mc`.columns.add(identNameToDb(`fieldName`)) + if `t`.`fieldIdent`.isSome: `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 # TODO -#macro rowToRecord(recType: untyped, row: seq[string]): untyped -# echo recType +macro rowToModel(modelType: typed): untyped = + result = newStmtList() -macro recordName(rec: typed): string = - return $rec.getTypeInst + #echo modelType.getType[1].getType.treeRepr + modelType.walkFieldDefs: + result.add createParseStmts(fieldType, "") + #[ + result.add quote do: + User( + id: genUUID + #modelType.walkFieldDefs: + ]# -proc tableNameForRecord[T](rec: T): string = - return recordName(rec).replace(UPPERCASE_PATTERN, "$1_$2").toLower() & "s" +macro modelName(model: typed): string = + 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 = 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 # we want from the row. let newIdStr = db.getValue(sql( - "INSERT INTO " & tableNameForRecord(rec) & + "INSERT INTO " & tableName(rec) & " (" & mc.columns.join(",") & ") " & " VALUES (" & mc.placeholders.join(",") & ") " & " 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 numRowsUpdated = db.execAffectedRows(sql( - "UPDATE " & tableNameForRecord(rec) & + "UPDATE " & tableName(rec) & " SET " & setClause & " WHERE id = ? "), mc.values.concat(@[rec.id])) @@ -138,33 +161,38 @@ proc updateRecord[T](db: DbConn, rec: T): bool = # TODO #proc getRecord[T](db: DbConn, UUID id): T = -macro listFieldNames(t: typed): untyped = - let tTypeImpl = t.getTypeImpl +macro listFields(t: typed): untyped = + 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): - 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) + result = newLit(fields) +# proc create: Typed create methods for specific records proc createUser*(db: DbConn, user: User): User = db.createRecord(user) proc createApiToken*(db: DbConn, token: ApiToken): ApiToken = db.createRecord(token) proc createMeasure*(db: DbConn, measure: Measure): Measure = db.createRecord(measure) proc createValue*(db: DbConn, value: Value): Value = db.createRecord(value) -#[ when isMainModule: + rowToModel(User) + let u = User( displayName: "Bob", email: "bob@bobsco.com", 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 ]# diff --git a/api/src/main/nim/personal_measure_apipkg/models.nim b/api/src/main/nim/personal_measure_apipkg/models.nim new file mode 100644 index 0000000..452aa95 --- /dev/null +++ b/api/src/main/nim/personal_measure_apipkg/models.nim @@ -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 diff --git a/api/src/main/sql/migrations/20190214122514-initial-schema-up.sql b/api/src/main/sql/migrations/20190214122514-initial-schema-up.sql index c4ad3f6..cfb51ce 100644 --- a/api/src/main/sql/migrations/20190214122514-initial-schema-up.sql +++ b/api/src/main/sql/migrations/20190214122514-initial-schema-up.sql @@ -5,7 +5,7 @@ create table "users" ( id uuid default uuid_generate_v4() primary key, display_name varchar not null, email varchar not null, - hashedpwd varchar not null + hashed_pwd varchar not null ); create table "api_tokens" (