summary refs log tree commit diff stats
path: root/examples/cross_todo/nimrod_backend/backend.nim
blob: 89e7d0b7e0ac9cfcd6762a47291f4f9f8d88a0da (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# Backend for a simple todo program with sqlite persistence.
#
# Most procs dealing with a TDbConn object may raise an EDb exception.

import db_sqlite, parseutils, strutils, times


type
  TTodo* = object
    ## A todo object holding the information serialized to the database.
    id: int64                 ## Unique identifier of the object in the
                              ## database, use the getId() accessor to read it.
    text*: string             ## Description of the task to do.
    priority*: int            ## The priority can be any user defined integer.
    isDone*: bool             ## Done todos are still kept marked.
    modificationDate: TTime   ## The modification time can't be modified from
                              ## outside of this module, use the
                              ## getModificationDate accessor.

  TPagedParams* = object
    ## Contains parameters for a query, initialize default values with
    ## initDefaults().
    pageSize*: int64          ## Lines per returned query page, -1 for
                              ## unlimited.
    priorityAscending*: bool  ## Sort results by ascending priority.
    dateAscending*: bool      ## Sort results by ascending modification date.
    showUnchecked*: bool      ## Get unchecked objects.
    showChecked*: bool        ## Get checked objects.


# - General procs
#
proc initDefaults*(params: var TPagedParams) =
  ## Sets sane defaults for a TPagedParams object.
  ##
  ## Note that you should always provide a non zero pageSize, either a specific
  ## positive value or negative for unbounded query results.
  params.pageSize = high(int64)
  params.priorityAscending = false
  params.dateAscending = false
  params.showUnchecked = true
  params.showChecked = false


proc openDatabase*(path: string): TDbConn =
  ## Creates or opens the sqlite3 database.
  ##
  ## Pass the path to the sqlite database, if the database doesn't exist it
  ## will be created. The proc may raise a EDB exception
  let
    conn = db_sqlite.open(path, "user", "pass", "db")
    query = sql"""CREATE TABLE IF NOT EXISTS Todos (
      id INTEGER PRIMARY KEY,
      priority INTEGER NOT NULL,
      is_done BOOLEAN NOT NULL,
      desc TEXT NOT NULL,
      modification_date INTEGER NOT NULL,
      CONSTRAINT Todos UNIQUE (id))"""

  db_sqlite.exec(conn, query)
  result = conn


# - Procs related to TTodo objects
#
proc initFromDB(id: int64; text: string; priority: int, isDone: bool;
               modificationDate: TTime): TTodo =
  ## Returns an initialized TTodo object created from database parameters.
  ##
  ## The proc assumes all values are right. Note this proc is NOT exported.
  assert(id >= 0, "Identity identifiers should not be negative")
  result.id = id
  result.text = text
  result.priority = priority
  result.isDone = isDone
  result.modificationDate = modificationDate


proc getId*(todo: TTodo): int64 =
  ## Accessor returning the value of the private id property.
  return todo.id


proc getModificationDate*(todo: TTodo): TTime =
  ## Returns the last modification date of a TTodo entry.
  return todo.modificationDate


proc update*(todo: var TTodo; conn: TDbConn): bool =
  ## Checks the database for the object and refreshes its variables.
  ##
  ## Use this method if you (or another entity) have modified the database and
  ## want to update the object you have with whatever the database has stored.
  ## Returns true if the update suceeded, or false if the object was not found
  ## in the database any more, in which case you should probably get rid of the
  ## TTodo object.
  assert(todo.id >= 0, "The identifier of the todo entry can't be negative")
  let query = sql"""SELECT desc, priority, is_done, modification_date
    FROM Todos WHERE id = ?"""

  try:
    let rows = conn.GetAllRows(query, $todo.id)
    if len(rows) < 1:
      return
    assert(1 == len(rows), "Woah, didn't expect so many rows")
    todo.text = rows[0][0]
    todo.priority = rows[0][1].parseInt
    todo.isDone = rows[0][2].parseBool
    todo.modificationDate = TTime(rows[0][3].parseInt)
    result = true
  except:
    echo("Something went wrong selecting for id " & $todo.id)


proc save*(todo: var TTodo; conn: TDbConn): bool =
  ## Saves the current state of text, priority and isDone to the database.
  ##
  ## Returns true if the database object was updated (in which case the
  ## modification date will have changed). The proc can return false if the
  ## object wasn't found, for instance, in which case you should drop that
  ## object anyway and create a new one with addTodo(). Also EDb can be raised.
  assert(todo.id >= 0, "The identifier of the todo entry can't be negative")
  let
    currentDate = getTime()
    query = sql"""UPDATE Todos
      SET desc = ?, priority = ?, is_done = ?, modification_date = ?
      WHERE id = ?"""
    rowsUpdated = conn.execAffectedRows(query, $todo.text,
      $todo.priority, $todo.isDone, $int(currentDate), $todo.id)

  if 1 == rowsUpdated:
    todo.modificationDate = currentDate
    result = true


# - Procs dealing directly with the database
#
proc addTodo*(conn: TDbConn; priority: int; text: string): TTodo =
  ## Inserts a new todo into the database.
  ##
  ## Returns the generated todo object. If there is an error EDb will be raised.
  let
    currentDate = getTime()
    query = sql"""INSERT INTO Todos
      (priority, is_done, desc, modification_date)
      VALUES (?, 'false', ?, ?)"""
    todoId = conn.insertId(query, priority, text, $int(currentDate))

  result = initFromDB(todoId, text, priority, false, currentDate)


proc deleteTodo*(conn: TDbConn; todoId: int64): int64 {.discardable.} =
  ## Deletes the specified todo identifier.
  ##
  ## Returns the number of rows which were affected (1 or 0)
  let query = sql"""DELETE FROM Todos WHERE id = ?"""
  result = conn.execAffectedRows(query, $todoId)


proc getNumEntries*(conn: TDbConn): int =
  ## Returns the number of entries in the Todos table.
  ##
  ## If the function succeeds, returns the zero or positive value, if something
  ## goes wrong a negative value is returned.
  let query = sql"""SELECT COUNT(id) FROM Todos"""
  try:
    let row = conn.getRow(query)
    result = row[0].parseInt
  except:
    echo("Something went wrong retrieving number of Todos entries")
    result = -1


proc getPagedTodos*(conn: TDbConn; params: TPagedParams;
                    page = 0'i64): seq[TTodo] =
  ## Returns the todo entries for a specific page.
  ##
  ## Pages are calculated based on the params.pageSize parameter, which can be
  ## set to a negative value to specify no limit at all.  The query will be
  ## affected by the TPagedParams, which should have sane values (call
  ## initDefaults).
  assert(page >= 0, "You should request a page zero or bigger than zero")
  result = @[]

  # Well, if you don't want to see anything, there's no point in asking the db.
  if not params.showUnchecked and not params.showChecked: return

  let
    order_by = [
      if params.priorityAscending: "ASC" else: "DESC",
      if params.dateAscending: "ASC" else: "DESC"]

    query = sql("""SELECT id, desc, priority, is_done, modification_date
      FROM Todos
      WHERE is_done = ? OR is_done = ?
      ORDER BY priority $1, modification_date $2, id DESC
      LIMIT ? * ?,?""" % order_by)

    args = @[$params.showChecked, $(not params.showUnchecked),
      $params.pageSize, $page, $params.pageSize]

  #echo("Query " & string(query))
  #echo("args: " & args.join(", "))

  var newId: biggestInt
  for row in conn.fastRows(query, args):
    let numChars = row[0].parseBiggestInt(newId)
    assert(numChars > 0, "Huh, couldn't parse identifier from database?")
    result.add(initFromDB(int64(newId), row[1], row[2].parseInt,
        row[3].parseBool, TTime(row[4].parseInt)))


proc getTodo*(conn: TDbConn; todoId: int64): ref TTodo =
  ## Returns a reference to a TTodo or nil if the todo could not be found.
  var tempTodo: TTodo
  tempTodo.id = todoId
  if tempTodo.update(conn):
    new(result)
    result[] = tempTodo