Add Ormin ORM support for BaraDB (Nim client)
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled

This commit is contained in:
2026-05-15 21:49:08 +03:00
parent 2e945c1dcb
commit 4e54075078
63 changed files with 9764 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
import std/strutils
import db_connector/db_common
proc dbTypFromName*(name: string): DbTypeKind =
var k = dbUnknown
case name.toLowerAscii
of "int", "integer", "int8", "smallint", "int16",
"longint", "int32", "int64", "tinyint", "hugeint": k = dbInt
of "uint", "uint8", "uint16", "uint32", "uint64": k = dbUInt
of "serial": k = dbSerial
of "bit": k = dbBit
of "bool", "boolean": k = dbBool
of "blob": k = dbBlob
of "fixedchar": k = dbFixedChar
of "varchar", "text", "string": k = dbVarchar
of "json": k = dbJson
of "xml": k = dbXml
of "decimal": k = dbDecimal
of "float", "double", "longdouble", "real": k = dbFloat
of "date", "day": k = dbDate
of "time": k = dbTime
of "datetime": k = dbDateTime
of "timestamp", "timestamptz": k = dbTimestamp
of "timeinterval": k = dbTimeInterval
of "set": k = dbSet
of "array": k = dbArray
of "composite": k = dbComposite
of "url", "uri": k = dbUrl
of "uuid": k = dbUuid
of "inet", "ip", "tcpip": k = dbInet
of "mac", "macaddress": k = dbMacAddress
of "geometry": k = dbGeometry
of "point": k = dbPoint
of "line": k = dbLine
of "lseg": k = dbLseg
of "box": k = dbBox
of "path": k = dbPath
of "polygon": k = dbPolygon
of "circle": k = dbCircle
else: discard
return k
+179
View File
@@ -0,0 +1,179 @@
import std/[os, paths]
import db_connector/db_common, strutils, strformat
import db_connector/db_postgres as db_postgres
import db_connector/db_sqlite as db_sqlite
import parsesql_tmp # import std/parsesql
export paths
type
DbConn = db_postgres.DbConn | db_sqlite.DbConn
DbId* = distinct string
DbSql* = distinct string
proc `$`*(dbId: DbId): string {.borrow.}
proc `$`*(dbSql: DbSql): string {.borrow.}
template staticLoad*(filename: string): DbSql =
bind isAbsolute, parentDir, `/`, instantiationInfo
block:
const schemaPath {.gensym.} = static:
if isAbsolute(filename):
filename
else:
parentDir(instantiationInfo(-1, true)[0]) / filename
const schemaSql {.gensym.} = staticRead(schemaPath)
static:
try:
discard parseSql(schemaSql)
except SqlParseError as e:
doAssert false, "Invalid SQL in " & schemaPath & ": " & e.msg
DbSql(schemaSql)
proc parseQualifiedIdentifier(name: string): seq[(string, bool)] =
var current = ""
var quoteChar = '\0'
var partWasQuoted = false
var i = 0
while i < name.len:
let c = name[i]
if quoteChar == '\0':
case c
of '.':
result.add((current, partWasQuoted))
current = ""
partWasQuoted = false
of '"', '\'', '`':
if current.len == 0:
quoteChar = c
partWasQuoted = true
else:
current.add(c)
else:
current.add(c)
else:
if c == quoteChar:
if i + 1 < name.len and name[i + 1] == quoteChar:
current.add(c)
inc(i)
else:
quoteChar = '\0'
else:
current.add(c)
inc(i)
result.add((current, partWasQuoted))
proc canUseBareIdentifier(part: string): bool =
if part.len == 0:
return false
if part[0] notin {'a'..'z', 'A'..'Z', '_'}:
return false
for c in part[1..^1]:
if c notin {'a'..'z', 'A'..'Z', '0'..'9', '_'}:
return false
true
proc quoteDbIdentifier*(name: string): DbId =
## Quote a database identifier for direct interpolation into SQL text.
## Dot-separated names are treated as qualified identifiers.
var partsSql: seq[string] = @[]
for (part, wasQuoted) in parseQualifiedIdentifier(name):
let normalizedPart = if wasQuoted: part else: part.toLowerAscii()
if wasQuoted or not canUseBareIdentifier(normalizedPart):
partsSql.add("\"" & normalizedPart.replace("\"", "\"\"") & "\"")
else:
partsSql.add(normalizedPart)
DbId(partsSql.join("."))
proc dropTableName(db: DbConn; tableName, lookupName: string) =
# SQL parameters bind values, not identifiers, so DROP TABLE needs the
# identifier rendered into the statement text with correct quoting.
when defined(sqlite) and defined(orminLegacySqliteDropNames):
db.exec(sql("drop table if exists " & lookupName))
else:
let tableIdentSql = quoteDbIdentifier(tableName)
when defined(postgre):
db.exec(sql("drop table if exists " & $tableIdentSql & " cascade"))
else:
db.exec(sql("drop table if exists " & $tableIdentSql))
iterator tableDefs(sql: DbSql): tuple[name, tableName, model: string] =
# Parse the entire SQL file and iterate statements via the SQL parser
var ast: SqlNode
try:
ast = parseSql($sql)
except SqlParseError as e:
echo "SQL Parse Error:\n", sql
raise e
if ast.len > 0:
# ast is a statement list; iterate each statement node
for i in 0 ..< ast.len:
let node = ast[i]
if node.kind in {nkCreateTable, nkCreateTableIfNotExists}:
yield (node[0].strVal.toLowerAscii(), $node[0], $node)
else:
# Fallback: ast might be a single statement (not a list)
let node = ast
if node.kind in {nkCreateTable, nkCreateTableIfNotExists}:
yield (node[0].strVal.toLowerAscii(), $node[0], $node)
iterator tablePairs*(sql: string): tuple[name, model: string] =
for name, _, model in tableDefs(DbSql(sql)):
yield (name, model)
iterator tablePairs*(sql: static[DbSql]): tuple[name, model: string] =
for name, _, model in tableDefs(sql):
yield (name, model)
iterator tablePairs*(sql: Path): tuple[name, model: string] =
let f = readFile($sql)
for n, m in tablePairs(f):
yield (n, m)
proc createTable*(db: DbConn; sqlFile: Path) =
for _, m in tablePairs(sqlFile):
db.exec(sql(m))
proc createTable*(db: DbConn; sqlFile: Path, name: string) =
for n, m in tablePairs(sqlFile):
if n == name:
db.exec(sql(m))
return
raiseAssert &"table: {name} not found in: {sqlFile}"
proc createTable*(db: DbConn; schemaSql: static[DbSql]) =
for _, m in tablePairs(schemaSql):
db.exec(sql(m))
proc createTable*(db: DbConn; schemaSql: static[DbSql], name: string) =
for n, m in tablePairs(schemaSql):
if n == name:
db.exec(sql(m))
return
raiseAssert &"table: {name} not found in static schema"
proc dropTable*(db: DbConn; sqlFile: Path) =
for lookupName, tableName, _ in tableDefs(DbSql(readFile($sqlFile))):
db.dropTableName(tableName, lookupName)
proc dropTable*(db: DbConn; sqlFile: Path, name: string) =
for lookupName, tableName, _ in tableDefs(DbSql(readFile($sqlFile))):
if lookupName == name:
db.dropTableName(tableName, lookupName)
return
raiseAssert &"table: {name} not found in: {sqlFile}"
proc dropTable*(db: DbConn; schemaSql: static[DbSql]) =
for lookupName, tableName, _ in tableDefs(schemaSql):
db.dropTableName(tableName, lookupName)
proc dropTable*(db: DbConn; schemaSql: static[DbSql], name: string) =
for lookupName, tableName, _ in tableDefs(schemaSql):
if lookupName == name:
db.dropTableName(tableName, lookupName)
return
raiseAssert &"table: {name} not found in static schema"
+71
View File
@@ -0,0 +1,71 @@
## This module implements a little helper macro to ease
## writing the dispatching logic for JSON based servers.
import macros
proc tbody(n: NimNode): NimNode =
# transforms::
# f fields(a, "b")
# into::
# f a = %args["a"], b = %args["b"]
case n.kind
of nnkCallKinds:
result = copyNimNode(n)
result.add tbody(n[0])
for i in 1..<n.len:
let it = n[i]
if it.kind == nnkCall and $it[0] == "fields":
for j in 1..<it.len:
let field = it[j]
let s = if field.kind in {nnkStrLit..nnkTripleStrLit}: field.strVal
else: $field
let arg = newTree(nnkPrefix, ident"%",
newTree(nnkBracketExpr, ident"arg", newLit(s)))
result.add newTree(nnkExprEqExpr, ident(s), arg)
else:
result.add tbody(it)
else:
result = copyNimNode(n)
for x in n: result.add tbody(x)
macro createDispatcher*(name, n: untyped): untyped =
expectKind n, nnkStmtList
result = newStmtList()
let disp = newTree(nnkCaseStmt, ident"cmd")
template cmdProc(name, body) {.dirty.} =
proc name(arg: JsonNode): JsonNode = body
template callCmd(name) {.dirty.} =
result = name(arg)
for x in n:
if x.kind == nnkCall and x.len == 2 and
x[0].kind == nnkIdent and x[1].kind == nnkStmtList:
result.add getAst(cmdProc(x[0], tbody x[1]))
disp.add newTree(nnkOfBranch, newLit($x[0]),
newStmtList(getAst(callCmd(x[0]))))
else:
# do not touch:
result.add n
disp.add newTree(nnkElse, newStmtList(newTree(nnkDiscardStmt, newEmptyNode())))
template dispatchProc(name, body) {.dirty.} =
proc dispatch(inp: JsonNode): JsonNode =
let arg = inp["arg"]
let cmd = inp["cmd"].getStr("")
body
result.add getAst(dispatchProc(name, disp))
when defined(debugDispatcherDsl):
echo repr result
when isMainModule:
import json, db_sqlite
createDispatcher(dispatch):
insertCustomer:
echo "insert customer", fields(a, b, c)
result = arg
selectCustomers:
echo "select customer"
result = arg
echo dispatch(nil, %*{"cmd": "insertCustomer", "arg": [1, 2, 3]})
+178
View File
@@ -0,0 +1,178 @@
import std/[macros, strutils, tables]
import db_connector/db_common
import ./db_types
import ./parsesql_tmp
const
FileHeader* = """
# Generated by ormin_importer. DO NOT EDIT.
type
Attr = object
name: string
tabIndex: int
typ: DbTypekind
key: int # 0 nothing special,
# +1 -- primary key
# -N -- references attribute N
"""
type
DbColumn* = object
name*: string
tableName*: string
typ*: DbType
primaryKey*: bool
refs*: (string, string)
DbColumns* = seq[DbColumn]
KnownTables* = OrderedTable[string, DbColumns]
ImportTarget* = enum
postgre, sqlite, mysql, baradb
proc hasAttribute(colDesc: SqlNode; k: set[SqlNodeKind]): bool =
for i in 2 ..< colDesc.len:
if colDesc[i].kind in k:
return true
proc hasRefs(colDesc: SqlNode): (string, string) =
for i in 2 ..< colDesc.len:
let c = colDesc[i]
if c.kind == nkReferences:
if c[0].kind == nkCall:
return (c[0][0].strVal, c[0][1].strVal)
elif c[0].kind == nkIdent:
return ($c[0], "id")
("", "")
proc getType(n: SqlNode): DbType =
var it = n
if it.kind == nkCall:
it = it[0]
if it.kind == nkEnumDef:
result.kind = dbEnum
result.validValues = @[]
for i in 0 ..< it.len:
assert it[i].kind == nkStringLit
result.validValues.add it[i].strVal
elif it.kind in {nkIdent, nkStringLit}:
result.kind = dbTypFromName(it.strVal)
result.name = it.strVal
proc collectTables*(n: SqlNode; t: var KnownTables) =
if n.isNil:
return
case n.kind
of nkCreateTable, nkCreateTableIfNotExists:
let tableName = n[0].strVal
var cols: DbColumns = @[]
for i in 1 ..< n.len:
let it = n[i]
if it.kind == nkColumnDef:
var typ = getType(it[1])
if hasAttribute(it, {nkNotNull}):
typ.notNull = true
cols.add DbColumn(
name: it[0].strVal,
tableName: tableName,
typ: typ,
primaryKey: hasAttribute(it, {nkPrimaryKey}),
refs: hasRefs(it)
)
for i in 1 ..< n.len:
let it = n[i]
if it.kind == nkForeignKey:
var refNode: SqlNode = nil
var localCols: seq[string] = @[]
for j in 0 ..< it.len:
let child = it[j]
if child.kind == nkReferences:
refNode = child
elif child.kind == nkIdent:
localCols.add(child.strVal)
if refNode.isNil:
continue
var r = refNode[0]
var refTable = ""
var refCols: seq[string] = @[]
if r.kind == nkColumnReference or r.kind == nkCall:
refTable = r[0].strVal
for k in 1 ..< r.len:
refCols.add(r[k].strVal)
let pairCount = min(localCols.len, refCols.len)
for k in 0 ..< pairCount:
let localName = localCols[k]
let targetCol = refCols[k]
for c in mitems(cols):
if cmpIgnoreCase(c.name, localName) == 0:
c.refs = (refTable, targetCol)
break
t[tableName] = cols
else:
for i in 0 ..< n.len:
collectTables(n[i], t)
proc attrToKey(a: DbColumn; t: KnownTables): int =
if a.primaryKey:
return 1
if a.refs[0].len > 0:
var i = 0
for k, v in pairs(t):
for b in v:
if cmpIgnoreCase(k, a.refs[0]) == 0 and cmpIgnoreCase(b.name, a.refs[1]) == 0:
return -i - 1
inc i
0
proc renderModelCode(schemaSql, schemaPath: string; target: ImportTarget; includeStatic = false): string =
discard target
let sql = parseSql(schemaSql, schemaPath)
var knownTables = initOrderedTable[string, DbColumns]()
collectTables(sql, knownTables)
result.add FileHeader
result.add "const tableNames = ["
var i = 0
for k in keys(knownTables):
if i > 0:
result.add ",\n "
else:
result.add "\n "
result.add escape(k.toLowerAscii)
inc i
result.add "\n]\n"
i = 0
var j = 0
result.add "\nconst attributes = ["
for _, v in mpairs(knownTables):
for a in v:
if j > 0:
result.add ",\n "
else:
result.add "\n "
result.add "Attr(name: "
result.add escape(a.name.toLowerAscii)
result.add ", tabIndex: "
result.add $i
result.add ", typ: "
result.add $a.typ.kind
result.add ", key: "
result.add $attrToKey(a, knownTables)
result.add ")"
inc j
inc i
result.add "\n]\n"
if includeStatic:
result.add "\nconst sqlSchema* = staticLoad("
result.add escape(schemaPath)
result.add ")\n"
proc generateModelCode*(schemaSql, schemaPath: string; target: ImportTarget; includeStatic = false): string =
renderModelCode(schemaSql, schemaPath, target, includeStatic)
macro generateModelCode*(schemaSql: static[string], schemaPath: static[string],
target: static[ImportTarget], includeStatic: static[bool] = false): untyped =
parseStmt(renderModelCode(schemaSql, schemaPath, target, includeStatic))
+231
View File
@@ -0,0 +1,231 @@
import std/[strutils, json, times, parseutils]
import db_connector/db_common
import query_hooks
export db_common
import baradb/client
export client.WireValue, client.FieldKind
type
DbConn* = SyncClient
PStmt = object
sql: string
varcharType* = string
intType* = int
floatType* = float
boolType* = bool
timestampType* = DateTime
serialType* = int
jsonType* = JsonNode
var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"
proc dbError*(db: DbConn) {.noreturn.} =
var e: ref DbError
new(e)
e.msg = "BaraDB query failed"
raise e
proc prepareStmt*(db: DbConn; q: string): PStmt =
when defined(debugOrminTrace):
echo "[[Ormin Executing]]: ", q
result.sql = q
template startBindings*(s: PStmt; n: int) {.dirty.} =
var pparams: seq[WireValue] = newSeq[WireValue](n)
template bindParam*(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyped) =
when t is DateTime:
let xx = x.format("yyyy-MM-dd HH:mm:ss")
pparams[idx-1] = WireValue(kind: fkString, strVal: $xx)
elif t is int or t is int64:
pparams[idx-1] = WireValue(kind: fkInt64, int64Val: int64(x))
elif t is float or t is float64:
pparams[idx-1] = WireValue(kind: fkFloat64, float64Val: float64(x))
elif t is bool:
pparams[idx-1] = WireValue(kind: fkBool, boolVal: bool(x))
elif t is string:
pparams[idx-1] = WireValue(kind: fkString, strVal: string(x))
elif t is JsonNode:
pparams[idx-1] = WireValue(kind: fkJson, jsonVal: $x)
else:
pparams[idx-1] = WireValue(kind: fkString, strVal: $x)
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
pparams[idx-1] = WireValue(kind: fkNull)
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
t: typedesc) =
let x = xx
if x.kind == JNull:
pparams[idx-1] = WireValue(kind: fkNull)
else:
bindFromJson(db, s, idx, x, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc) =
{.error: "invalid type for JSON object".}
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[string]) =
doAssert x.kind == JString
pparams[idx-1] = WireValue(kind: fkString, strVal: x.str)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[int|int64]) =
doAssert x.kind == JInt
pparams[idx-1] = WireValue(kind: fkInt64, int64Val: x.num)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[float64]) =
doAssert x.kind == JFloat
pparams[idx-1] = WireValue(kind: fkFloat64, float64Val: x.fnum)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[bool]) =
doAssert x.kind == JBool
pparams[idx-1] = WireValue(kind: fkBool, boolVal: x.bval)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[DateTime]) =
doAssert x.kind == JString
pparams[idx-1] = WireValue(kind: fkString, strVal: x.str)
template startQuery*(db: DbConn; s: PStmt) =
when declared(pparams):
var queryResult {.inject.} = db.query(s.sql, pparams)
else:
var queryResult {.inject.} = db.query(s.sql)
var queryI {.inject.} = -1
var queryLen {.inject.} = queryResult.rowCount
template stopQuery*(db: DbConn; s: PStmt) =
discard
template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
inc queryI
queryI < queryLen
template getLastId*(db: DbConn; s: PStmt): int =
0
template getAffectedRows*(db: DbConn; s: PStmt): int =
queryResult.affectedRows
proc close*(db: DbConn) =
client.close(db)
proc open*(connection, user, password, database: string): DbConn =
let colonPos = connection.find(':')
let host = if colonPos < 0: connection else: connection[0..<colonPos]
let portStr = if colonPos < 0: "9472" else: connection[colonPos+1..^1]
let port = parseInt(portStr)
let cfg = ClientConfig(
host: host,
port: port,
database: database,
username: user,
password: password
)
result = newSyncClient(cfg)
result.connect()
# --- Result binding helpers ---
template currentValue(s: PStmt; idx: int): string =
queryResult.rows[queryI][idx-1]
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
currentValue(s, idx).len == 0
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
t: typedesc; name: string) =
dest = int(parseBiggestInt(currentValue(s, idx)))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
t: typedesc; name: string) =
dest = parseBiggestInt(currentValue(s, idx))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
t: typedesc; name: string) =
let v = currentValue(s, idx)
dest = v == "true" or v == "1" or v == "t"
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
t: typedesc; name: string) =
dest = currentValue(s, idx)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
t: typedesc; name: string) =
dest = parseFloat(currentValue(s, idx))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var DateTime;
t: typedesc; name: string) =
let src = currentValue(s, idx)
if src.len > 0:
dest = parse(src, "yyyy-MM-dd HH:mm:ss")
else:
dest = initDateTime(1, 1, 1, 0, 0, 0, utc())
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
t: typedesc; name: string) =
dest = parseJson(currentValue(s, idx))
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
t: typedesc; name: string) =
let v = currentValue(s, idx)
if v.len == 0:
dest.isNull = true
else:
dest.isNull = false
when T is string:
dest.value = v
else:
bindResult(db, s, idx, dest.value, t, name)
template createJObject*(): untyped = newJObject()
template createJArray*(): untyped = newJArray()
template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
let x = obj
doAssert x.kind == JObject
let v = currentValue(s, idx)
if v.len == 0:
x[name] = newJNull()
else:
bindToJson(db, s, idx, x, t, name)
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
{.error: "invalid type for JSON object".}
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[string]; name: string) =
obj[name] = newJString(currentValue(s, idx))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[int|int64]; name: string) =
var v: int64
discard parseBiggestInt(currentValue(s, idx), v)
obj[name] = newJInt(v)
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[float64]; name: string) =
obj[name] = newJFloat(parseFloat(currentValue(s, idx)))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[bool]; name: string) =
let v = currentValue(s, idx)
obj[name] = newJBool(v == "true" or v == "1" or v == "t")
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[DateTime]; name: string) =
var dt: DateTime
bindResult(db, s, idx, dt, t, name)
obj[name] = newJString(format(dt, jsonTimeFormat))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[JsonNode]; name: string) =
obj[name] = parseJson(currentValue(s, idx))
+274
View File
@@ -0,0 +1,274 @@
import strutils, db_connector/postgres, json, times
import db_connector/db_common
import query_hooks
export db_common
type
DbConn* = PPGconn ## encapsulates a database connection
PStmt = string ## a identifier for the prepared queries
varcharType* = string
intType* = int
floatType* = float
boolType* = bool
timestampType* = Datetime
serialType* = int
jsonType* = JsonNode
var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"
proc dbError*(db: DbConn) {.noreturn.} =
## raises a DbError exception.
var e: ref DbError
new(e)
e.msg = $pqErrorMessage(db)
raise e
proc c_strtod(buf: cstring, endptr: ptr cstring = nil): float64 {.
importc: "strtod", header: "<stdlib.h>", noSideEffect.}
proc c_strtol(buf: cstring, endptr: ptr cstring = nil, base: cint = 10): int {.
importc: "strtol", header: "<stdlib.h>", noSideEffect.}
var sid {.compileTime.}: int
proc prepareStmt*(db: DbConn; q: string): PStmt =
when defined(debugOrminTrace):
echo "[[Ormin Executing]]: ", q
inc sid
result = "ormin" & $sid
var res = pqprepare(db, result, q, 0, nil)
if pqResultStatus(res) != PGRES_COMMAND_OK: dbError(db)
template startBindings*(s: PStmt; n: int) {.dirty.} =
# pparams is a duplicated array to keep the Nim string alive
# for the duration of the query. This is safer than relying
# on the conservative stack marking:
var pparams: array[n, string]
var parr: array[n, cstring]
template bindParam*(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyped) =
# when not (x is t):
# {.error: "type mismatch for query argument at position " & $idx.}
when t is DateTime:
let xx = x.format("yyyy-MM-dd HH:mm:ss\'.\'ffffffzzzz")
pparams[idx-1] = $xx
else:
pparams[idx-1] = $x
parr[idx-1] = cstring(pparams[idx-1])
template bindParamUnchecked(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyped) =
pparams[idx-1] = $x
parr[idx-1] = cstring(pparams[idx-1])
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
parr[idx-1] = cstring(nil)
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
t: typedesc) =
let x = xx
if x.kind == JNull:
# a NULL entry is not reflected in the 'pparams' array:
parr[idx-1] = cstring(nil)
else:
bindFromJson(db, s, idx, x, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc) =
{.error: "invalid type for JSON object".}
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[string]) =
doAssert x.kind == JString
let xs = x.str
bindParamUnchecked(db, s, idx, xs, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[int|int64]) =
doAssert x.kind == JInt
let xi = x.num
bindParamUnchecked(db, s, idx, xi, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[float64]) =
doAssert x.kind == JFloat
let xf = x.fnum
bindParamUnchecked(db, s, idx, xf, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[bool]) =
doAssert x.kind == JBool
let xb = x.bval
bindParamUnchecked(db, s, idx, xb, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[DateTime]) =
doAssert x.kind == JString
let dt = x.str
bindParamUnchecked(db, s, idx, dt, t)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
t: typedesc; name: string) =
dest = c_strtol(pqgetvalue(queryResult, queryI, idx.cint))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
t: typedesc; name: string) =
dest = c_strtol(pqgetvalue(queryResult, queryI, idx.cint))
# XXX This needs testing:
template isTrue(x): untyped = x[0] == 't'
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
t: typedesc; name: string) =
dest = isTrue(pqgetvalue(queryResult, queryI, idx.cint))
proc fillString(dest: var string; src: cstring; srcLen: int) {.inline.} =
dest = ""
var i = 0
while src[i] != '\0':
dest.add src[i]
inc i
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
t: typedesc; name: string) =
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
when defined(nimNoNilSeqs):
setLen(dest, 0)
else:
dest = nil
else:
let src = pqgetvalue(queryResult, queryI, idx.cint)
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
fillString(dest, src, srcLen)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
t: typedesc; name: string) =
dest = c_strtod(pqgetvalue(queryResult, queryI, idx.cint))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var DateTime;
t: typedesc; name: string) =
let
src = $pqgetvalue(queryResult, queryI, idx.cint)
i = src.find('.')
tzflag = src[src.len-3]
if i < 0:
if tzflag in {'+', '-'}:
dest = parse(src, "yyyy-MM-dd HH:mm:sszz")
else:
dest = parse(src, "yyyy-MM-dd HH:mm:ss")
else:
if tzflag in {'+', '-'}:
let itz = src.len - 3
let dtstr = src[0..<itz] & '0'.repeat(10-src.len+i) & src[src.len-3 .. ^1]
dest = parse(dtstr, "yyyy-MM-dd HH:mm:ss\'.\'ffffffzz")
else:
let dtstr = src & '0'.repeat(7-src.len+i)
dest = parse(dtstr, "yyyy-MM-dd HH:mm:ss\'.\'ffffff")
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
t: typedesc; name: string) =
dest = parseJson($pqgetvalue(queryResult, queryI, idx.cint))
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
t: typedesc; name: string) =
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
dest.isNull = true
else:
dest.isNull = false
when T is string:
let src = pqgetvalue(queryResult, queryI, idx.cint)
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
fillString(dest.value, src, srcLen)
else:
bindResult(db, s, idx, dest.value, t, name)
template createJObject*(): untyped = newJObject()
template createJArray*(): untyped = newJArray()
template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
let x = obj
doAssert x.kind == JObject
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
x[name] = newJNull()
else:
bindToJson(db, s, idx, x, t, name)
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
pqgetisnull(queryResult, queryI, idx.cint) != 0
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
{.error: "invalid type for JSON object".}
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[string]; name: string) =
let src = pqgetvalue(queryResult, queryI, idx.cint)
obj[name] = newJString($src)
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[int|int64]; name: string) =
obj[name] = newJInt(c_strtol(pqgetvalue(queryResult, queryI, idx.cint)))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[float64]; name: string) =
obj[name] = newJFloat(c_strtod(pqgetvalue(queryResult, queryI, idx.cint)))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[bool]; name: string) =
obj[name] = newJBool(isTrue(pqgetvalue(queryResult, queryI, idx.cint)))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[DateTime]; name: string) =
var dt: DateTime
bindResult(db, s, idx, dt, t, name)
obj[name] = newJString(format(dt, jsonTimeFormat))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[JsonNode]; name: string) =
let src = pqgetvalue(queryResult, queryI, idx.cint)
obj[name] = parseJson($src)
template startQuery*(db: DbConn; s: PStmt) =
when declared(pparams):
var queryResult {.inject.} = pqexecPrepared(db, s, int32(parr.len),
cast[cstringArray](addr parr), nil, nil, 0)
else:
var queryResult {.inject.} = pqexecPrepared(db, s, int32(0),
nil, nil, nil, 0)
if pqResultStatus(queryResult) == PGRES_COMMAND_OK:
discard # insert does not returns data in pg
elif pqResultStatus(queryResult) != PGRES_TUPLES_OK: ormin_postgre.dbError(db)
var queryI {.inject.} = cint(-1)
var queryLen {.inject.} = pqntuples(queryResult)
template stopQuery*(db: DbConn; s: PStmt) =
pqclear(queryResult)
template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
inc queryI
queryI < queryLen
template getLastId*(db: DbConn; s: PStmt): int = 0 # XXX to implement
template getAffectedRows*(db: DbConn; s: PStmt): int =
c_strtol(pqcmdTuples(queryResult))
proc close*(db: DbConn) =
## closes the database connection.
if db != nil: pqfinish(db)
proc open*(connection, user, password, database: string): DbConn =
## opens a database connection. Raises `DbError` if the connection could not
## be established.
let
colonPos = connection.find(':')
host = if colonPos < 0: connection
else: substr(connection, 0, colonPos-1)
port = if colonPos < 0: ""
else: substr(connection, colonPos+1)
result = pqsetdbLogin(host, port, nil, nil, database, user, password)
if pqStatus(result) != CONNECTION_OK: dbError(result) # result = nil
+295
View File
@@ -0,0 +1,295 @@
{.deadCodeElim: on.}
import json, times
import db_connector/db_common
import db_connector/sqlite3
import query_hooks
export db_common
type
DbConn* = PSqlite3 ## encapsulates a database connection
varcharType* = string
blobType* = seq[byte]
intType* = int
floatType* = float
boolType* = bool
timestampType* = DateTime
jsonType* = JsonNode
var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"
proc dbError*(db: DbConn) {.noreturn.} =
## raises a DbError exception.
var e: ref DbError
new(e)
e.msg = $sqlite3.errmsg(db)
raise e
proc prepareStmt*(db: DbConn; q: string): PStmt =
when defined(debugOrminTrace):
echo "[[Ormin Executing]]: ", q
if prepare_v2(db, q, q.len.cint, result, nil) != SQLITE_OK:
dbError(db)
template startBindings*(s: PStmt; n: int) =
if clear_bindings(s) != SQLITE_OK: dbError(db)
template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) =
#when not (x is t):
# {.error: "type mismatch for query argument at position " & $idx.}
when t is blobType:
let xs = x
let blobPtr = if xs.len == 0:
cast[pointer](nil)
else:
cast[pointer](unsafeAddr(xs[0]))
if bind_blob(s, idx.cint, blobPtr, xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
elif t is int or t is int64 or t is bool:
if bind_int64(s, idx.cint, x.int64) != SQLITE_OK: dbError(db)
elif t is string:
if bind_text(s, idx.cint, cstring(x), x.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
elif t is float64:
if bind_double(s, idx.cint, x) != SQLITE_OK:
dbError(db)
elif t is DateTime:
let xx = if x.nanosecond > 0:
x.utc().format("yyyy-MM-dd HH:mm:ss\'.\'fff")
else:
x.utc().format("yyyy-MM-dd HH:mm:ss")
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
elif t is JsonNode:
let xx = $x
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
else:
{.error: "type mismatch for query argument at position " & $idx.}
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
if bind_null(s, idx.cint) != SQLITE_OK:
dbError(db)
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
t: typedesc) =
let x = xx
if x.kind == JNull:
if bind_null(s, idx.cint) != SQLITE_OK: dbError(db)
else:
bindFromJson(db, s, idx, x, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc) =
{.error: "invalid type for JSON object".}
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[string]) =
doAssert x.kind == JString
let xs = x.str
if bind_text(s, idx.cint, cstring(xs), xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[int|int64]) =
doAssert x.kind == JInt
let xi = x.num
if bind_int64(s, idx.cint, xi.int64) != SQLITE_OK: dbError(db)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[float64]) =
doAssert x.kind == JFloat
let xf = x.fnum
if bind_double(s, idx.cint, xf) != SQLITE_OK:
dbError(db)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[bool]) =
doAssert x.kind == JBool
let xb = x.bval
if bind_int64(s, idx.cint, xb.int64) != SQLITE_OK: dbError(db)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[DateTime]) =
doAssert x.kind == JString
let
dtStr = x.str
i = dtStr.find('.')
dt = if i > 0 and dtStr[i+1 .. ^1] == "000":
dtStr[0 ..< i]
else:
dtStr
if bind_text(s, idx.cint, cstring(dt), dt.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
t: typedesc; name: string) =
dest = int column_int64(s, idx.cint)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
t: typedesc; name: string) =
dest = column_int64(s, idx.cint)
proc fillString(dest: var string; src: cstring; srcLen: int) =
when defined(nimNoNilSeqs):
setLen(dest, srcLen)
else:
if dest.isNil: dest = newString(srcLen)
else: setLen(dest, srcLen)
if srcLen > 0:
copyMem(unsafeAddr(dest[0]), src, srcLen)
proc fillBytes(dest: var seq[byte]; src: pointer; srcLen: int) =
when defined(nimNoNilSeqs):
setLen(dest, srcLen)
else:
if dest.isNil: dest = newSeq[byte](srcLen)
else: setLen(dest, srcLen)
if srcLen > 0:
copyMem(unsafeAddr(dest[0]), src, srcLen)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
t: typedesc; name: string) =
if column_type(s, idx.cint) == SQLITE_NULL:
when defined(nimNoNilSeqs):
setLen(dest, 0)
else:
dest = nil
else:
let srcLen = column_bytes(s, idx.cint)
let src = column_text(s, idx.cint)
fillString(dest, src, srcLen)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var blobType;
t: typedesc; name: string) =
let srcLen = column_bytes(s, idx.cint)
let src = column_blob(s, idx.cint)
fillBytes(dest, src, srcLen)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
t: typedesc; name: string) =
dest = column_double(s, idx.cint)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
t: typedesc; name: string) =
dest = column_int64(s, idx.cint) != 0
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: DateTime;
t: typedesc; name: string) =
let
src = $column_text(s, idx.cint)
i = src.find('.')
if i < 0:
dest = parse(src, "yyyy-MM-dd HH:mm:ss", utc())
else:
dest = parse(src, "yyyy-MM-dd HH:mm:ss\'.\'fff", utc())
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
t: typedesc; name: string) =
let src = column_text(s, idx.cint)
dest = parseJson($src)
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
t: typedesc; name: string) =
if column_type(s, idx.cint) == SQLITE_NULL:
dest.isNull = true
else:
dest.isNull = false
when T is string:
let srcLen = column_bytes(s, idx.cint)
let src = column_text(s, idx.cint)
fillString(dest.value, src, srcLen)
else:
bindResult(db, s, idx, dest.value, t, name)
template createJObject*(): untyped = newJObject()
template createJArray*(): untyped = newJArray()
template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
let x = obj
doAssert x.kind == JObject
if column_type(s, idx.cint) == SQLITE_NULL:
x[name] = newJNull()
else:
bindToJson(db, s, idx, x, t, name)
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
column_type(s, idx.cint) == SQLITE_NULL
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
{.error: "invalid type for JSON object".}
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[string]; name: string) =
let dest = newJString("")
let srcLen = column_bytes(s, idx.cint)
let src = column_text(s, idx.cint)
fillString(dest.str, src, srcLen)
obj[name] = dest
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[int|int64]; name: string) =
obj[name] = newJInt(column_int64(s, idx.cint))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[blobType]; name: string) =
let srcLen = column_bytes(s, idx.cint)
let src = column_blob(s, idx.cint)
var data: blobType
fillBytes(data, src, srcLen)
let arr = newJArray()
for b in data:
arr.add newJInt(int(b))
obj[name] = arr
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[float64]; name: string) =
obj[name] = newJFloat(column_double(s, idx.cint))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[bool]; name: string) =
obj[name] = newJBool(column_int64(s, idx.cint) != 0)
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[DateTime]; name: string) =
var dt: DateTime
bindResult(db, s, idx, dt, t, name)
obj[name] = newJString(format(dt, jsonTimeFormat))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[JsonNode]; name: string) =
let src = column_text(s, idx.cint)
obj[name] = parseJson($src)
template startQuery*(db: DbConn; s: PStmt) = discard "nothing to do"
template stopQuery*(db: DbConn; s: PStmt) =
if sqlite3.reset(s) != SQLITE_OK: dbError(db)
template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
when returnsData:
step(s) == SQLITE_ROW
else:
step(s) == SQLITE_DONE
template getLastId*(db: DbConn; s: PStmt): int =
int(last_insert_rowid(db))
template getAffectedRows*(db: DbConn; s: PStmt): int =
int(changes(db))
proc close*(db: DbConn) =
## closes the database connection.
if sqlite3.close(db) != SQLITE_OK: dbError(db)
proc open*(connection, user, password, database: string): DbConn =
## opens a database connection. Raises `EDb` if the connection could not
## be established. Only the ``connection`` parameter is used for ``sqlite``.
if sqlite3.open(connection, result) != SQLITE_OK:
dbError(result)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
import options, json
type
DbValue*[T] = object
isNull*: bool
value*: T
template fromQueryHook*[T, S](val: var T, x: S) =
## Default conversion hook used by `query(T): ...`.
## Users can overload this proc to customize field/type conversions.
val = x
template toQueryHook*[T, S](val: var T, x: S) =
## Default conversion hook used for query parameters.
## Users can overload this proc to customize parameter conversions.
val = x
proc nullQueryValueError() {.noreturn.} =
raise newException(ValueError, "cannot map NULL query result")
proc fromQueryHook*[T, S](val: var Option[T], x: var DbValue[S]) =
if x.isNull:
val = none(T)
else:
var converted: T
fromQueryHook(converted, move x.value)
val = some(converted)
proc fromQueryHook*[T, S](val: var T, x: var DbValue[S]) =
if x.isNull:
when T is string:
val = ""
elif T is JsonNode:
val = newJNull()
else:
nullQueryValueError()
else:
fromQueryHook(val, move x.value)
proc bindFromQueryHook*[T, S](dest: var T, x: var DbValue[S]) =
fromQueryHook(dest, x)
proc toQueryHook*[S, T](val: var DbValue[S], x: Option[T]) =
if x.isSome:
val.isNull = false
toQueryHook(val.value, x.get)
else:
val.isNull = true
when compiles(val.value = default(S)):
val.value = default(S)
proc toQueryHook*[S, T](val: var DbValue[S], x: T) =
val.isNull = false
toQueryHook(val.value, x)
+139
View File
@@ -0,0 +1,139 @@
import asynchttpserver, asyncdispatch, asyncnet, json,
strutils, times
import websocket
type
Receivers* {.pure} = enum
sender, allExceptSender, all
ReqHandler* = proc (msg: JsonNode; receivers: var Receivers): JsonNode {.
closure, gcsafe.}
proc error(msg: string) = echo "[Error] ", msg
proc warn(msg: string) = echo "[Warning] ", msg
proc hint(msg: string) = echo "[Hint] ", msg
type
Client = ref object
socket: AsyncWebSocket
connected: bool
hostname: string
id: int
lastMessage: float
rapidMessageCount: int
Server = ref object
clients: seq[Client]
needsUpdate: bool
receivers: Receivers
msgs: seq[(string, int)]
gid: int
handler: ReqHandler
proc newClient(s: Server; socket: AsyncWebSocket, hostname: string): Client =
inc s.gid
result = Client(socket: socket, connected: true, hostname: hostname, id: s.gid)
proc `$`(client: Client): string =
"Client(ip: $1)" % [client.hostname]
proc updateClients(server: Server) {.async.} =
while true:
var needsUpdate = false
for client in server.clients:
if not client.connected:
needsUpdate = true
break
server.needsUpdate = server.needsUpdate or needsUpdate
if server.needsUpdate and server.msgs.len != 0:
var someDead = false
# perform a copy to prevent the race condition:
var msgs = server.msgs
setLen(server.msgs, 0)
for m in msgs:
for c in server.clients:
if c.connected:
# do not send a broadcast message to the one which sent it:
if server.receivers != Receivers.allExceptSender or c.id != m[1]:
await c.socket.sendText(m[0]) #, false)
else:
someDead = true
if someDead:
var i = 0
while i < server.clients.len:
if not server.clients[i].connected: del(server.clients, i)
else: inc i
server.needsUpdate = false
# let other stuff in the main loop run:
await sleepAsync(10)
proc processMessage(server: Server, client: Client, data: string) {.async.} =
# Check if last message was relatively recent. If so, kick the user.
echo "processMessage ", data
let now = epochTime()
if now - client.lastMessage < 0.1: # 100ms
client.rapidMessageCount.inc
else:
client.rapidMessageCount = 0
client.lastMessage = now
if client.rapidMessageCount > 50:
warn("Client ($1) is firing messages too rapidly. Killing." % $client)
client.connected = false
let msgj = parseJson(data)
server.receivers = Receivers.sender
if msgj.hasKey("msg") and msgj["msg"].getStr("") == "disconnect":
client.connected = false
server.needsUpdate = true
else:
let resp = server.handler(msgj, server.receivers)
if not resp.isNil:
if server.receivers == sender:
await client.socket.sendText($resp) #, false)
else:
server.msgs.add(($resp, client.id))
server.needsUpdate = true
proc processClient(server: Server, client: Client) {.async.} =
while client.connected:
var frameFut = client.socket.readData()
yield frameFut
if frameFut.failed:
error("Error occurred handling client messages.\n" &
frameFut.error.msg)
client.connected = false
break
let frame = frameFut.read()
if frame.opcode == Opcode.Text:
let processFut = processMessage(server, client, frame.data)
if processFut.failed:
echo processFut.error.getStackTrace()
error("Client ($1) attempted to send bad JSON? " % $client & "\n" &
processFut.error.msg)
#client.connected = false
await client.socket.close()
proc onRequest(server: Server, req: Request; key: string) {.async.} =
let (ws, error) = await verifyWebsocketRequest(req, key)
if ws != nil:
hint("Client connected from " & req.hostname)
ws.protocol = key
server.clients.add(newClient(server, ws, req.hostname))
asyncCheck processClient(server, server.clients[^1])
else:
warn("WS negotiation failed: " & $error)
await req.respond(Http400, "WebSocket negotiation failed: " & $error)
req.client.close()
proc serve*(key: string; handler: ReqHandler) =
let httpServer = newAsyncHttpServer()
let server = Server(clients: @[], msgs: @[], handler: handler, gid: 0)
proc cb(req: Request): Future[void] {.async, gcsafe.} =
await onRequest(server, req, key)
asyncCheck updateClients(server)
waitFor httpServer.serve(Port(8080), cb)