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
@@ -0,0 +1,35 @@
## Basic Ormin + BaraDB example
##
## Run with:
## nim c -r examples/baradb_basic.nim
##
## Requires a BaraDB server on localhost:9472.
import ormin
importModel(DbBackend.baradb, "baradb_model")
let db {.global.} = open("127.0.0.1:9472", "admin", "", "default")
proc listUsers() =
let rows = query:
select users(id, name, email)
orderby id
for r in rows:
echo "User #", r.id, ": ", r.name, " <", r.email, ">"
proc findUserByName(name: string) =
let row = query:
select users(id, name, email)
where name == ?name
limit 1
echo "Found: ", row
proc insertUser(name, email: string; age: int) =
query:
insert users(name = ?name, email = ?email, age = ?age)
when isMainModule:
listUsers()
findUserByName("alice")
insertUser("bob", "bob@example.com", 30)
@@ -0,0 +1,6 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255),
age INT
);
@@ -0,0 +1,19 @@
create table if not exists users(
id integer primary key,
name varchar(20) not null,
password varchar(32) not null,
creation timestamp not null default (DATETIME('now')),
lastOnline timestamp not null default (DATETIME('now'))
);
/* Names need to be unique: */
create unique index if not exists UserNameIx on users(name);
create table if not exists messages(
id integer primary key,
author integer not null,
content varchar(1000) not null,
creation timestamp not null default (DATETIME('now')),
foreign key (author) references users(id)
);
@@ -0,0 +1,9 @@
import strutils, db_sqlite
var db = open(connection="chat.db", user="araq", password="",
database="chat")
let model = readFile("chat_model.sql")
for m in model.split(';'):
if m.strip != "":
db.exec(sql(m), [])
db.close()
@@ -0,0 +1,92 @@
## Frontend for our chat application example.
## You need to have 'karax' installed in order for this to work.
import karax / [kbase, karax, vdom, karaxdsl, kajax, jwebsockets, jjson,
jstrutils, errors]
# Custom UI element with input validation:
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field)
if x.text.isNil or x.text == "":
setError(field, field & " must not be empty")
else:
setError(field, "")
proc loginField(desc, field, class: kstring;
validator: proc (field: kstring): proc ()): VNode =
result = buildHtml(tdiv):
label(`for` = field):
text desc
input(`type` = class, id = field, onchange = validator(field))
# here we setup the connection to the server:
let conn = newWebSocket("ws://localhost:8080", "orminchat")
proc send(msg: JsonNode) =
# The Ormin "protocol" requires us to have 'send' implementation.
conn.send(toJson(msg))
type User* = ref object
name*, password: kstring
const
username = kstring"username"
password = kstring"password"
message = kstring"message"
# Include the 'chatclient' helper that Ormin produced for us:
include chatclient
template loggedIn(): bool = userId > 0
proc doLogin() =
registerUser(User(name: getVNodeById(username).text,
password: getVNodeById(password).text))
proc doSendMessage() =
let inputField = getVNodeById(message)
sendMessage(TextMessage(author: userId, content: inputField.text))
inputField.setInputText ""
proc registerOnUpdate() =
conn.onmessage =
proc (e: MessageEvent) =
let msg = fromJson[JsonNode](e.data)
# 'recvMsg' was generated for us:
recvMsg(msg)
karax.redraw()
proc main(): VNode =
result = buildHtml(tdiv):
tdiv:
if not loggedIn:
loginField("Name :", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = doLogin, disabled = disableOnError()):
text "Login"
p:
text getError(username)
p:
text getError(password)
tdiv:
table:
for m in allMessages:
tr:
td:
bold:
text m.name
td:
text m.content
tdiv:
if loggedIn:
label(`for` = message):
text "Message: "
input(class = "input", id = message, onkeyupenter = doSendMessage)
registerOnUpdate()
runLater proc() =
getRecentMessages()
setRenderer(main)
@@ -0,0 +1,96 @@
import ../../ormin / [serverws]
import ../../ormin
import json
# Ormin needs to know about our SQL model:
importModel(DbBackend.sqlite, "chat_model")
# Currently Ormin assumes a global database connection that needs to be
# annotated as '.global' for technical reasons. Later versions will improve.
var db {.global.} = open("chat.db", "", "", "")
# Ormin produces the file "chatclient.nim" for our frontend:
protocol "chatclient.nim":
# A 'common' code section is shared by both the client and the server:
common:
when not defined(js):
type kstring = string
type
inetType = kstring
varcharType = kstring
timestampType = kstring
intType = int
server "get recent messages":
let lastMessages = query:
select messages(content, creation, author)
join users(name)
orderby desc(creation)
limit 100
send(lastMessages)
client "get recent messages":
type TextMessage* = ref object
# Ormin fills in the fields of 'TextMessage' for us, based on the
# query in the 'server' part.
var allMessages*: seq[TextMessage] = @[]
proc getRecentMessages*()
allMessages = recv()
server "send chat message":
let userId = arg["author"].num
# unregistered users cannot send anything:
if userId == 0: return
query:
insert messages(content = %arg["content"], author = ?userId)
query:
update users(lastOnline = !!"DATETIME('now')")
where id == ?userId
let lastMessage = query:
select messages(content, creation, author)
join users(name)
orderby desc(creation)
limit 1
broadcast(lastMessage)
client "send chat message":
proc sendMessage*(m: TextMessage)
allMessages.add recv(TextMessage)
# This is the request to register/login a new user:
server "register/login a new user":
var candidates = query:
produce nim
select users(id, password)
where name == %arg["name"]
if candidates.len == 0:
let userId = query:
produce nim
insert users(name = %arg["name"], password = %arg["password"])
returning id
send(%userId)
else:
block search:
for c in candidates:
if c[1] == arg["password"].str:
send(%c[0])
echo "login found!"
break search
# invalid login:
send(%0)
echo "no such user!"
client "register/login a new user":
proc registerUser*(u: User)
var userId: int
userId = recv()
if userId == 0: setError(username, "invalid login!")
serve "orminchat", dispatch
@@ -0,0 +1,2 @@
--define: ssl
--path:"$projectDir/../../../ormin"
@@ -0,0 +1,2 @@
--define: ssl
--path:"$projectDir/../../../ormin"
+160
View File
@@ -0,0 +1,160 @@
import ../../ormin, json
importModel(DbBackend.sqlite, "forum_model")
var db {.global.} = open("stuff", "", "", "")
#var db: DbConn
#proc getPrepStmt(idx: int): PStmt
#var gPrepStmts: array[N, cstring]
type inetType = string
const
id = 90
ip = "moo"
answer = "dunno"
pw = "mypw"
email = "some@body.com"
salt = "pepper"
name = "me"
limit = 10
offset = 5
let threads = query:
select thread(id, name, views, modified)
where id in (select post(thread) where author in
(select person(id) where status notin ("Spammer") or id == ?id))
orderby desc(modified)
limit ?limit
offset ?offset
let thisThread = tryQuery:
select thread(id)
where id == ?id
createIter allThreadIds:
select thread(id)
where id == ?id
query:
delete antibot
where ip == ?ip
query:
insert antibot(?ip, ?answer)
let something = query:
select antibot(answer & answer, (if ip == "hi": 0 else: 1))
where ip == ?ip and answer =~ "%things%"
orderby desc(ip)
limit 1
let myNewPersonId: int = query:
insert person(?name, password = ?pw, ?email, ?salt, status = !!"'EmailUnconfirmed'",
lastOnline = !!"DATETIME('now')")
returning id
query:
delete session
where ip == ?ip and password == ?pw
query:
update session(lastModified = !!"DATETIME('now')")
where ip == ?ip and password == ?pw
let myj = %*{"pw": "stuff here"}
let userId1 = query:
select session(userId)
where ip == ?ip and password == %myj["pw"]
let (name9, email9, status, ban) = query:
select person(name, email, status, ban)
where id == ?id
limit 1
let (idg, nameg, pwg, emailg, creationg, saltg, statusg, lastOnlineg, bang) = query:
select person(_)
where id == ?id
limit 1
query:
update person(lastOnline = !!"DATETIME('now')")
where id == ?id
query:
update thread(views = views + 1, modified = !!"DATETIME('now')")
where id == ?id
query:
delete thread
where id notin (select post(thread))
let (author, creation) = query:
select post(author)
join person(creation)
limit 1
let (authorB, creationB) = query:
select post(author)
join person(creation) on author == id
limit 1
let allPosts = query:
select post(count(_) as cnt)
where cnt > 0
produce json
limit 1
createProc getAllThreadIds:
select thread(id)
where id == ?id
produce json
let totalThreads = query:
select thread(count(_))
where id in (select post(thread) where author == ?id and id in (
select post(min(id)) groupby thread))
limit 1
#query:
# update thread(modified = (select post(creation) where post.thread == ?thread
# orderby creation desc limit 1 ))
#[
# Check if post is the first post of the thread.
let rows = db.getAllRows(sql("select id, thread, creation from post " &
"where thread = ? order by creation asc"), $c.threadId)
proc rateLimitCheck(c: var TForumData): bool =
sql("SELECT count(*) FROM post where author = ? and " &
"(strftime('%s', 'now') - strftime('%s', creation)) < 40")
sql("SELECT count(*) FROM post where author = ? and " &
"(strftime('%s', 'now') - strftime('%s', creation)) < 90")
sql("SELECT count(*) FROM post where author = ? and " &
"(strftime('%s', 'now') - strftime('%s', creation)) < 300")
sql "insert into thread(name, views, modified) values (?, 0, DATETIME('now'))"
sql "select id, name, password, email, salt, status, ban from person where name = ?"
sql "insert into session (ip, password, userid) values (?, ?, ?)",
sql"select password, salt, strftime('%s', lastOnline) from person where name = ?"
sql("delete from post where author = (select id from person where name = ?)")
sql("update person set status = ?, ban = ? where name = ?")
sql("update person set password = ?, salt = ? where name = ?")
sql"select count(*) from person"
sql"select count(*) from post"
sql"select count(*) from thread"
sql"select id, name, strftime('%s', lastOnline), strftime('%s', creation) from person"
sql"select count(*) from post where thread = ?"
sql"select count(*) from post p, person u where u.id = p.author and p.thread = ?"
sql"select name from thread where id = ?"
sql"select id from person where name = ?"
sql"select count(*) from post where author = ?"
sql("select count(*) from thread where id in (select thread from post where" &
" author = ? and post.id in (select min(id) from post group by thread))")
sql"""select strftime('%s', lastOnline), email, ban, status
from person where id = ?"""
]#
@@ -0,0 +1,55 @@
create table if not exists thread(
id integer primary key,
name varchar(100) not null,
views integer not null,
modified timestamp not null default (DATETIME('now'))
);
create unique index if not exists ThreadNameIx on thread (name);
create table if not exists person(
id integer primary key,
name varchar(20) not null,
password varchar(32) not null,
email varchar(30) not null,
creation timestamp not null default (DATETIME('now')),
salt varchar(128) not null,
status varchar(30) not null,
lastOnline timestamp not null default (DATETIME('now')),
ban varchar(128) not null default ''
);
create unique index if not exists UserNameIx on person (name);
create table if not exists post(
id integer primary key,
author integer not null,
ip inet not null,
header varchar(100) not null,
content varchar(1000) not null,
thread integer not null,
creation timestamp not null default (DATETIME('now')),
foreign key (thread) references thread(id),
foreign key (author) references person(id)
);
create table if not exists session(
id integer primary key,
ip inet not null,
password varchar(32) not null,
userid integer not null,
lastModified timestamp not null default (DATETIME('now')),
foreign key (userid) references person(id)
);
create table if not exists antibot(
id integer primary key,
ip inet not null,
answer varchar(30) not null,
created timestamp not null default (DATETIME('now'))
);
create index PersonStatusIdx on person(status);
create index PostByAuthorIdx on post(thread, author);
@@ -0,0 +1,37 @@
import ../../ormin, ../../ormin/serverws, json
importModel(DbBackend.sqlite, "forum_model")
var db {.global.} = open("stuff", "", "", "")
protocol "forumclient.nim":
common:
when defined(js):
type kstring = cstring
else:
type kstring = string
type
inetType = kstring
varcharType = kstring
timestampType = kstring
server:
query:
delete antibot
where ip == %arg
client:
proc deleteAntibot(ip: string)
server:
query:
update session(lastModified = !!"DATETIME('now')")
where ip == %arg["ip"] and password == %arg["pw"]
client:
proc updateSession(arg: Session)
server:
let allSessions = query:
select session(_)
send(allSessions)
client:
type Session = ref object
var gSessions: seq[Session]
gSessions = recv()
proc getAllSessions()
@@ -0,0 +1,117 @@
body {
background-color: #f1f9ea;
margin: 0;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}
div#main {
width: 80%;
margin-left: auto;
margin-right: auto;
}
div#user {
background-color: #66ac32;
width: 100%;
color: #c7f0aa;
padding: 5pt;
}
div#user > h1 {
color: #ffffff;
}
h1 {
margin: 0;
display: inline;
padding-left: 10pt;
padding-right: 10pt;
}
div#user > form {
float: right;
margin-right: 10pt;
}
div#user > form > input[type="submit"] {
border: 0px none;
padding: 5pt;
font-size: 108%;
color: #ffffff;
background-color: #515d47;
border-radius: 5px;
cursor: pointer;
}
div#user > form > input[type="submit"]:hover {
background-color: #538c29;
}
div#messages {
background-color: #a2dc78;
width: 90%;
margin-left: auto;
margin-right: auto;
color: #1a1a1a;
}
div#messages > div {
border-left: 1px solid #869979;
border-right: 1px solid #869979;
border-bottom: 1px solid #869979;
padding: 5pt;
}
div#messages > div > a, div#messages > div > span {
color: #475340;
}
div#messages > div > a:hover {
text-decoration: none;
color: #c13746;
}
h3 {
margin-bottom: 0;
font-weight: normal;
}
div#login {
width: 200px;
margin-left: auto;
margin-right: auto;
margin-top: 20%;
font-size: 130%;
}
div#login span.small {
display: block;
font-size: 56%;
}
div#newMessage {
background-color: #538c29;
width: 90%;
margin-left: auto;
margin-right: auto;
color: #ffffff;
padding: 5pt;
}
div#newMessage span {
padding-right: 5pt;
}
div#newMessage form {
display: inline;
}
div#newMessage > form > input[type="text"] {
width: 80%;
}
div#newMessage > form > input[type="submit"] {
font-size: 80%;
}
@@ -0,0 +1,3 @@
# Tweeter example
Modified from [tweeter example](https://github.com/dom96/nim-in-action-code/tree/master/Chapter7/Tweeter) in the "nim in action" book, replace the database layer with ormin.
@@ -0,0 +1,2 @@
--define: ssl
--path:"$projectDir/../../../../ormin"
@@ -0,0 +1,11 @@
import db_sqlite, os, strutils
let db {.global.} = open("tweeter.db", "", "", "")
let sqlFile = readFile(currentSourcePath.parentDir() / "tweeter_model.sql")
for t in sqlFile.split(';'):
if t.strip() != "":
db.exec(sql(t))
echo("Database created successfully!")
db.close()
@@ -0,0 +1,51 @@
import times, strutils, strformat, sequtils
from db_connector/db_sqlite import instantRows, `[]`
import ../../../ormin
import model
importModel(DbBackend.sqlite, "tweeter_model")
var db {.global.} = open("tweeter.db", "", "", "")
proc findUser*(username: string, user: var User): bool =
let res = query:
select user(username)
where username == ?username
echo res
if res.len == 0: return false
else: user.username = res[0]
let following = query:
select following(followed_user)
where follower == ?username
user.following = following.filterIt(it.len != 0)
return true
proc create*(user: User) =
query:
insert user(username = ?user.username)
proc post*(message: Message) =
if message.msg.len > 140:
raise newException(ValueError, "Message has to be less than 140 characters.")
query:
insert message(username = ?message.username,
time = ?message.time,
msg = ?message.msg)
proc follow*(follower, user: User) =
query:
insert following(follower = ?follower.username, followed_user = ?user.username)
proc findMessage*(usernames: openArray[string], limit = 10): seq[Message] =
result = @[]
if usernames.len == 0: return
var whereClause = "WHERE "
for i in 0 ..< usernames.len:
whereClause.add("trim(username) = ?")
if i < usernames.len - 1:
whereClause.add(" or ")
let s = &"SELECT username, time, msg FROM Message {whereClause} ORDER BY time DESC LIMIT {limit}"
for row in db.instantRows(sql(s), usernames):
result.add((username: row[0], time: row[1].parseInt, msg: row[2]))
@@ -0,0 +1,11 @@
type
User* = tuple[
username: string,
following: seq[string]
]
Message* = tuple[
username: string,
time: int,
msg: string
]
@@ -0,0 +1,59 @@
import asyncdispatch, times
import jester
import database, model, views/[user, general]
proc userLogin(request: Request, user: var User): bool =
if request.cookies.hasKey("username"):
let username = request.cookies["username"]
if not findUser(username, user):
user = (username: username, following: @[])
create(user)
return true
else:
return false
routes:
get "/":
var user: User
if userLogin(request, user):
let messages = findMessage(user.following & user.username)
resp renderMain(renderTimeline(user.username, messages))
else:
resp renderMain(renderLogin())
get "/@name":
cond '.' notin @"name"
var user: User
if not findUser(@"name", user):
halt "User not found"
let messages = findMessage([user.username])
var currentUser: User
if userLogin(request, currentUser):
resp renderMain(renderUser(user, currentUser) & renderMessages(messages))
else:
resp renderMain(renderUser(user) & renderMessages(messages))
post "/follow":
var follower, target: User
if not findUser(@"follower", follower):
halt "Follower not found"
if not findUser(@"target", target):
halt "Follow target not found"
follow(follower, target)
redirect uri("/" & @"target")
post "/login":
setCookie("username", @"username", getTime().utc() + 2.hours)
redirect "/"
post "/createMessage":
let message = (
username: @"username",
time: getTime().toUnix().int,
msg: @"message"
)
post(message)
redirect "/"
runForever()
@@ -0,0 +1,24 @@
# 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
const tableNames = [
"user",
"following",
"message"
]
const attributes = [
Attr(name: "username", tabIndex: 0, typ: dbVarchar, key: 1),
Attr(name: "follower", tabIndex: 1, typ: dbVarchar, key: -1),
Attr(name: "followed_user", tabIndex: 1, typ: dbVarchar, key: -1),
Attr(name: "username", tabIndex: 2, typ: dbVarchar, key: -1),
Attr(name: "time", tabIndex: 2, typ: dbInt, key: 0),
Attr(name: "msg", tabIndex: 2, typ: dbVarchar, key: 0)
]
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS User(
username text PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS Following(
follower text,
followed_user text,
PRIMARY KEY (follower, followed_user),
FOREIGN KEY (follower) REFERENCES User(username),
FOREIGN KEY (followed_user) REFERENCES User(username)
);
CREATE TABLE IF NOT EXISTS Message(
username text,
time integer,
msg text NOT NULL,
FOREIGN KEY (username) REFERENCES User(username)
);
@@ -0,0 +1,51 @@
#? stdtmpl(subsChar = '$', metaChar = '#')
#import xmltree
#import ../model
#import user
#
#proc `$!`(text: string): string = escape(text)
#end proc
#
#proc renderMain*(body: string): string =
# result = ""
<!DOCTYPE html>
<html>
<head>
<title>Tweeter written in Nim</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
${body}
</body>
</html>
#end proc
#
#proc renderLogin*(): string =
# result = ""
<div id="login">
<span>Login</span>
<span class="small">Please type in your username...</span>
<form action="login" method="post">
<input type="text" name="username">
<input type="submit" value="Login">
</form>
</div>
#end proc
#
#proc renderTimeline*(username: string, messages: openArray[Message]): string =
# result = ""
<div id="user">
<h1>${$!username} timeline</h1>
</div>
<div id="newMessage">
<span>New message</span>
<form action="createMessage" method="post">
<input type="text" name="message">
<input type="hidden" name="username" value="${$!username}">
<input type="submit" value="Tweet">
</form>
</div>
${renderMessages(messages)}
#end proc
@@ -0,0 +1,41 @@
#? stdtmpl(subsChar = '$', metaChar = '#', toString = "xmltree.escape")
#import xmltree
#import times
#import "../model"
#
#proc renderUser*(user: User): string =
# result = ""
<div id="user">
<h1>${user.username}</h1>
<span>Following: ${$user.following.len}</span>
</div>
#end proc
#
#proc renderUser*(user, currentUser: User): string =
# result = ""
<div id="user">
<h1>${user.username}</h1>
<span>Following: ${$user.following.len}</span>
#if user.username notin currentUser.following and user.username != currentUser.username:
<form action="follow" method="post">
<input type="hidden" name="follower" value="${currentUser.username}">
<input type="hidden" name="target" value="${user.username}">
<input type="submit" value="Follow">
</form>
#end if
</div>
#
#end proc
#
#proc renderMessages*(messages: openArray[Message]): string =
# result = ""
<div id="messages">
#for message in messages:
<div>
<a href="/${message.username}">${message.username}</a>
<span>${message.time.fromUnix().format("HH:mm MMMM d',' yyyy")}</span>
<h3>${message.msg}</h3>
</div>
#end for
</div>
#end proc