WIP Logging service and API.

This commit is contained in:
2019-03-02 02:04:51 -06:00
parent fc8dbd3fb7
commit e9bdcbffcd
16 changed files with 331 additions and 28 deletions

View File

@ -153,7 +153,9 @@ proc makeAuthToken*(ctx: PMApiContext, email, pwd: string): string =
let users = ctx.db.findUsersByEmail(email)
if users.len != 1: raiseEx ""
user = users[0]
except: raiseEx AuthError, "invalid username or password"
except:
error "unable to find user", getCurrentExceptionMsg()
raiseEx AuthError, "invalid username or password"
if not validatePwd(user, pwd): raiseEx AuthError, "invalid username or password"
@ -510,6 +512,41 @@ proc start*(ctx: PMApiContext): void =
error "unable to delete measurement:\n\t" & getCurrentExceptionMsg()
jsonResp(Http500)
post "/log":
checkAuth()
try:
let jsonBody = parseJson(request.body)
let logEntry = ClientLogEntry(
userId: session.user.id,
level: jsonBody.getOrFail("level").getStr,
message: jsonBody.getOrFail("message").getStr,
scope: jsonBody.getOrFail("scope").getStr,
stacktrace: jsonBody.getIfExists("stacktrace").getStr(""),
timestamp: jsonBody.getOrFail("timestampe").getStr.parseIso8601
)
resp(Http200, $(%ctx.db.createClientLogEntry(logEntry)), JSON)
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
except: jsonResp(Http500, getCurrentExceptionMsg())
post "/log/batch":
checkAuth()
try:
let jsonBody = parseJson(request.body);
let respMsgs = jsonBody.getElems.mapIt(
ClientLogEntry(
userId: session.user.id,
level: it.getOrFail("level").getStr,
message: it.getOrFail("message").getStr,
scope: it.getOrFail("scope").getStr,
stacktrace: it.getIfExists("stacktrace").getStr(""),
timestamp: it.getOrFail("timestampe").getStr.parseIso8601
))
resp(Http200, $(%respMsgs), JSON)
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
except: jsonResp(Http500, getCurrentExceptionMsg())
post "/service/debug/stop":
if not ctx.cfg.debug: jsonResp(Http404)
else:

View File

@ -14,7 +14,7 @@ type
proc connect*(connString: string): PMApiDb =
result = PMApiDb(conn: open("", "", "", connString))
generateProcsForModels([User, ApiToken, Measure, Measurement])
generateProcsForModels([User, ApiToken, Measure, Measurement, ClientLogEntry])
generateLookup(User, @["email"])
@ -27,3 +27,5 @@ generateLookup(Measure, @["userId", "slug"])
generateLookup(Measurement, @["measureId"])
generateLookup(Measurement, @["measureId", "id"])
generateLookup(ClientLogEntry, @["userId"])

View File

@ -18,14 +18,13 @@ 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(
let newRow = db.getRow(sql(
"INSERT INTO " & tableName(rec) &
" (" & mc.columns.join(",") & ") " &
" VALUES (" & mc.placeholders.join(",") & ") " &
" RETURNING id"), mc.values)
" RETURNING *"), mc.values)
result = rec
result.id = parseUUID(newIdStr)
result = rowToModel(T, newRow)
proc updateRecord*[T](db: DbConn, rec: T): bool =
var mc = newMutateClauses()
@ -39,13 +38,13 @@ proc updateRecord*[T](db: DbConn, rec: T): bool =
return numRowsUpdated > 0;
template deleteRecord*(db: DbConn, modelType: type, id: UUID): untyped =
template deleteRecord*(db: DbConn, modelType: type, id: typed): untyped =
db.tryExec(sql("DELETE FROM " & tableName(modelType) & " WHERE id = ?"), $id)
proc deleteRecord*[T](db: DbConn, rec: T): bool =
return db.tryExec(sql("DELETE FROM " & tableName(rec) & " WHERE id = ?"), $rec.id)
template getRecord*(db: DbConn, modelType: type, id: UUID): untyped =
template getRecord*(db: DbConn, modelType: type, id: typed): untyped =
let row = db.getRow(sql(
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType) &
@ -88,15 +87,16 @@ macro generateProcsForModels*(modelTypes: openarray[type]): untyped =
let createName = ident("create" & modelName)
let updateName = ident("update" & modelName)
let deleteName = ident("delete" & modelName)
let idType = typeOfColumn(t, "id")
result.add quote do:
proc `getName`*(db: PMApiDb, id: UUID): `t` = getRecord(db.conn, `t`, id)
proc `getName`*(db: PMApiDb, id: `idType`): `t` = getRecord(db.conn, `t`, id)
proc `getAllName`*(db: PMApiDb): seq[`t`] = getAllRecords(db.conn, `t`)
proc `findWhereName`*(db: PMApiDb, whereClause: string, values: varargs[string, dbFormat]): seq[`t`] =
return findRecordsWhere(db.conn, `t`, whereClause, values)
proc `createName`*(db: PMApiDb, rec: `t`): `t` = createRecord(db.conn, rec)
proc `updateName`*(db: PMApiDb, rec: `t`): bool = updateRecord(db.conn, rec)
proc `deleteName`*(db: PMApiDb, rec: `t`): bool = deleteRecord(db.conn, rec)
proc `deleteName`*(db: PMApiDb, id: UUID): bool = deleteRecord(db.conn, `t`, id)
proc `deleteName`*(db: PMApiDb, id: `idType`): bool = deleteRecord(db.conn, `t`, id)
macro generateLookup*(modelType: type, fields: seq[string]): untyped =
let fieldNames = fields[1].mapIt($it)

View File

@ -10,6 +10,13 @@ type
placeholders*: seq[string]
values*: seq[string]
# TODO: more complete implementation
# see https://github.com/blakeembrey/pluralize
proc pluralize(name: string): string =
if name[^2..^1] == "ey": return name[0..^3] & "ies"
if name[^1] == 'y': return name[0..^2] & "ies"
return name & "s"
macro modelName*(model: object): string =
return $model.getTypeInst
@ -38,10 +45,10 @@ proc dbNameToIdent*(name: string): string =
return @[parts[0]].concat(parts[1..^1].mapIt(capitalize(it))).join("")
proc tableName*(modelType: type): string =
return modelName(modelType).identNameToDb & "s"
return pluralize(modelName(modelType).identNameToDb)
proc tableName*[T](rec: T): string =
return modelName(rec).identNameToDb & "s"
return pluralize(modelName(rec).identNameToDb)
proc dbFormat*(s: string): string = return s
@ -131,8 +138,7 @@ proc createParseStmt*(t, value: NimNode): NimNode =
if `value`.len == 0: none[`innerType`]()
else: some(`parseStmt`)
else:
error "Unknown value object type: " & $t.getTypeInst
else: error "Unknown value object type: " & $t.getTypeInst
elif t.typeKind == ntyRef:
@ -211,6 +217,24 @@ macro listFields*(t: typed): untyped =
result = newLit(fields)
proc typeOfColumn*(modelType: NimNode, colName: string): NimNode =
modelType.walkFieldDefs:
if $fieldIdent != colName: continue
if fieldType.typeKind == ntyObject:
if fieldType.getType == UUID.getType: return ident("UUID")
elif fieldType.getType == DateTime.getType: return ident("DateTime")
elif fieldType.getType == Option.getType: return ident("Option")
else: error "Unknown column type: " & $fieldType.getTypeInst
else: return fieldType
raise newException(Exception,
"model of type '" & $modelType & "' has no column named '" & colName & "'")
proc isZero(val: int): bool = return val == 0
macro populateMutateClauses*(t: typed, newRecord: bool, mc: var MutateClauses): untyped =
result = newStmtList()
@ -249,6 +273,3 @@ macro populateMutateClauses*(t: typed, newRecord: bool, mc: var MutateClauses):
`mc`.columns.add(identNameToDb(`fieldName`))
`mc`.placeholders.add("?")
`mc`.values.add(dbFormat(`t`.`fieldIdent`))
#echo result.repr

View File

@ -35,6 +35,15 @@ type
timestamp*: DateTime
extData*: JsonNode
ClientLogEntry* = object
id*: int
userId*: UUID
level*: string
message*: string
scope*: string
stacktrace*: string
timestamp*: DateTime
proc `$`*(u: User): string =
return "User " & ($u.id)[0..6] & " - " & u.displayName & " <" & u.email & ">"
@ -49,6 +58,10 @@ proc `$`*(m: Measure): string =
proc `$`*(v: Measurement): string =
return "Measurement " & ($v.id)[0..6] & " - " & ($v.measureId)[0..6] & " = " & $v.value
proc `%`*(uuid: UUID): JsonNode = %($uuid)
proc `%`*(dt: DateTime): JsonNode = %(dt.formatIso8601)
proc `%`*(u: User): JsonNode =
result = %*{
"id": $u.id,
@ -81,12 +94,3 @@ proc `%`*(m: Measure): JsonNode =
if m.domainSource.isSome: result["domainSource"] = %(m.domainSource.get)
if m.rangeSource.isSome: result["rangeSource"] = %(m.rangeSource.get)
proc `%`*(v: Measurement): JsonNode =
result = %*{
"id": $v.id,
"measureId": $v.measureId,
"value": v.value,
"timestamp": v.timestamp.formatIso8601,
"extData": v.extData
}