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
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:
@@ -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"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user