-
Notifications
You must be signed in to change notification settings - Fork 1
/
db.go
381 lines (312 loc) · 8.51 KB
/
db.go
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package main
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
)
type DB struct {
sqlDB *sql.DB
CollFSM *FSM
TaskFSM *FSM
// collection
createColl *sql.Stmt
readColl *sql.Stmt
readColls *sql.Stmt
readState *sql.Stmt
updateCollData *sql.Stmt
updateCollState *sql.Stmt
deleteColl *sql.Stmt
// event
createEvent *sql.Stmt
readEvents *sql.Stmt
deleteEvents *sql.Stmt
// task
createTask *sql.Stmt
readTasks *sql.Stmt
updateTaskState *sql.Stmt
deleteTasks *sql.Stmt
}
func NewDB(sqlDB *sql.DB, collFSM, taskFSM *FSM) (*DB, error) {
var db = &DB{
sqlDB: sqlDB,
CollFSM: collFSM,
TaskFSM: taskFSM,
}
_, err := sqlDB.Exec(`
create table if not exists coll (
id text primary key,
pass text not null,
state text not null,
data text not null
);
create table if not exists event (
id integer primary key,
collid text not null,
collstate text not null,
date text not null,
paid INTEGER NOT NULL,
text text not null
);
create table if not exists task (
id text primary key,
collid text not null,
state text not null,
data text not null
);
`)
if err != nil {
return nil, err
}
// collection
db.createColl, err = db.sqlDB.Prepare("insert into coll (id, pass, state, data) values (?, ?, ?, ?)")
if err != nil {
return nil, err
}
db.readColl, err = db.sqlDB.Prepare("select pass, state, data from coll where id = ? limit 1")
if err != nil {
return nil, err
}
db.readColls, err = db.sqlDB.Prepare("select id from coll where coll.state = ?")
if err != nil {
return nil, err
}
db.readState, err = db.sqlDB.Prepare("select state from coll where id = ? limit 1")
if err != nil {
return nil, err
}
db.updateCollData, err = db.sqlDB.Prepare("update coll set data = ? where id = ?")
if err != nil {
return nil, err
}
db.updateCollState, err = db.sqlDB.Prepare("update coll set state = ? where id = ?")
if err != nil {
return nil, err
}
db.deleteColl, err = db.sqlDB.Prepare("delete from coll where id = ?")
if err != nil {
return nil, err
}
// event
db.createEvent, err = db.sqlDB.Prepare("insert into event (collid, collstate, date, paid, text) values (?, ?, ?, ?, ?)")
if err != nil {
return nil, err
}
db.readEvents, err = db.sqlDB.Prepare("select collstate, date, paid, text FROM event where collid = ? order by id desc")
if err != nil {
return nil, err
}
db.deleteEvents, err = db.sqlDB.Prepare("delete from event where collid = ?")
if err != nil {
return nil, err
}
// task
db.createTask, err = db.sqlDB.Prepare("insert or replace into task (id, collid, state, data) values (?, ?, ?, ?)") // not upsert, which is useful for partial updates but not required here
if err != nil {
return nil, err
}
db.readTasks, err = db.sqlDB.Prepare("select id, state, data from task where collid = ?")
if err != nil {
return nil, err
}
db.updateTaskState, err = db.sqlDB.Prepare("update task set state = ? where id = ?")
if err != nil {
return nil, err
}
db.deleteTasks, err = db.sqlDB.Prepare("delete from task where collid = ?")
if err != nil {
return nil, err
}
return db, nil
}
func (db *DB) CreateCollection(coll *Collection) error {
tx, err := db.sqlDB.Begin()
if err != nil {
return err
}
defer tx.Rollback() // no effect after commit
if _, err := tx.Stmt(db.createColl).Exec(coll.ID, coll.Pass, Draft, "{}"); err != nil {
return err
}
var firstEvent = Event{ // required for the bot which deletes old drafts
NewState: Draft,
Date: Today(),
Paid: 0,
Text: "Auftragsentwurf wurde angelegt",
}
coll.Log = []Event{firstEvent}
if _, err := tx.Stmt(db.createEvent).Exec(coll.ID, firstEvent.NewState, firstEvent.Date, firstEvent.Paid, firstEvent.Text); err != nil {
return err
}
return tx.Commit()
}
// CreateEvent creates an event. UpdateCollState should be preferred if the collection state changes.
func (db *DB) CreateEvent(actor Actor, coll *Collection, paid int, message string) error {
message = strings.TrimSpace(message)
if message != "" {
message = fmt.Sprintf("%s: %s", actor.Name(), message)
}
_, err := db.createEvent.Exec(coll.ID, coll.State, Today(), paid, message)
return err
}
func (db *DB) Delete(actor Actor, coll *Collection) error {
if !db.CollFSM.Can(actor, State(coll.State), State(Deleted)) {
return errors.New("not allowed to delete collection") // deletion is important, so we must state clearly if it fails (and not just return ErrNotFound)
}
tx, err := db.sqlDB.Begin()
if err != nil {
return err
}
defer tx.Rollback() // no effect after commit
if _, err := tx.Stmt(db.deleteColl).Exec(coll.ID); err != nil {
return err
}
if _, err := tx.Stmt(db.deleteEvents).Exec(coll.ID); err != nil {
return err
}
if _, err := tx.Stmt(db.deleteTasks).Exec(coll.ID); err != nil {
return err
}
return tx.Commit()
}
func (db *DB) ReadColl(id string) (*Collection, error) {
var collData string
var coll = &Collection{ID: id}
if err := db.readColl.QueryRow(id).Scan(&coll.Pass, &coll.State, &collData); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(collData), &coll.CollectionData); err != nil {
return nil, err
}
// events
events, err := db.readEvents.Query(id)
if err != nil {
return nil, err
}
defer events.Close()
for events.Next() {
var event = Event{}
if err := events.Scan(&event.NewState, &event.Date, &event.Paid, &event.Text); err != nil {
return nil, err
}
coll.Log = append(coll.Log, event)
}
// tasks
tasks, err := db.readTasks.Query(id)
if err != nil {
return nil, err
}
defer tasks.Close()
for tasks.Next() {
var taskData string
var task = &Task{}
if err := tasks.Scan(&task.ID, &task.State, &taskData); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(taskData), &task.TaskData); err != nil {
return nil, err
}
coll.Tasks = append(coll.Tasks, task)
}
return coll, nil
}
func (db *DB) ReadCollPass(id, pass string) (*Collection, error) {
var coll, err = db.ReadColl(id)
if err != nil {
return nil, err
}
if !coll.CompareHash(pass) {
return nil, ErrNotFound
}
return coll, nil
}
// task count is currently filtered with NotOrderedYet
func (db *DB) ReadColls(state CollState) ([]string, error) {
rows, err := db.readColls.Query(state)
if err != nil {
return nil, err
}
defer rows.Close()
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, nil
}
func (db *DB) ReadState(id string) (CollState, error) {
var state string
return CollState(state), db.readState.QueryRow(id).Scan(&state)
}
// coll must contain the old state
func (db *DB) UpdateCollState(actor Actor, coll *Collection, newState CollState, paidAmount int, message string) error {
if !db.CollFSM.Can(actor, State(coll.State), State(newState)) {
return ErrNotFound
}
message = strings.TrimSpace(message)
if message != "" {
message = fmt.Sprintf("%s: %s", actor.Name(), message)
}
tx, err := db.sqlDB.Begin()
if err != nil {
return err
}
defer tx.Rollback() // no effect after commit
if _, err := tx.Stmt(db.updateCollState).Exec(newState, coll.ID); err != nil {
return err
}
if _, err := tx.Stmt(db.createEvent).Exec(coll.ID, newState, Today(), paidAmount, message); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
coll.State = newState
return nil
}
// updates the collection given by coll.ID
func (db *DB) UpdateCollAndTasks(coll *Collection) error {
data, err := json.Marshal(coll.CollectionData)
if err != nil {
return err
}
tx, err := db.sqlDB.Begin()
if err != nil {
return err
}
defer tx.Rollback() // no effect after commit
if _, err := tx.Stmt(db.updateCollData).Exec(string(data), coll.ID); err != nil {
return err
}
if _, err := tx.Stmt(db.deleteTasks).Exec(coll.ID); err != nil {
return err
}
for _, task := range coll.Tasks {
if task.State == "" {
task.State = NotOrderedYet // initial state
}
taskData, err := json.Marshal(task.TaskData)
if err != nil {
return err
}
if _, err := tx.Stmt(db.createTask).Exec(task.ID, coll.ID, task.State, string(taskData)); err != nil {
return err
}
}
return tx.Commit()
}
// task must contain the old state
func (db *DB) UpdateTaskState(actor Actor, task *Task, newState TaskState) error {
if !db.TaskFSM.Can(actor, State(task.State), State(newState)) {
return ErrNotFound
}
if _, err := db.updateTaskState.Exec(newState, task.ID); err != nil {
return err
}
task.State = newState
return nil
}