e1ec9fbf Andy McCall 1 month ago ↶
1
2
3
use super :: { now_ms, Pool} ;
4
use sqlx:: FromRow;
5
6
# [ derive ( Clone, Debug, FromRow ) ]
7
pub struct Project {
8
pub id : i64 ,
9
pub owner : String,
10
pub number : i64 ,
11
pub title : String,
12
pub description : String,
13
pub public : bool ,
14
pub closed : bool ,
15
pub created_ms : i64 ,
16
pub updated_ms : i64 ,
17
}
18
19
# [ derive ( Clone, Debug, FromRow ) ]
20
pub struct Column {
21
pub id : i64 ,
22
pub project_id : i64 ,
23
pub name : String,
24
pub position : i64 ,
439ecd87 Andy McCall 11 days ago ↶
25
26
pub archive_after_days : Option< i64 > ,
1000b40a Andy McCall 10 days ago ↶
27
28
29
pub takes_finished : bool ,
0bc95135 Andy McCall 2 days ago ↶
30
31
#[sqlx(default)]
32
pub wip_limit : Option< i64 > ,
e1ec9fbf Andy McCall 1 month ago ↶
33
}
34
35
# [ derive ( Clone, Debug, FromRow ) ]
36
pub struct Card {
37
pub id : i64 ,
38
pub project_id : i64 ,
39
pub column_id : i64 ,
40
pub position : i64 ,
41
pub kind : String,
42
pub repo_id : Option< i64 > ,
43
pub number : Option< i64 > ,
44
pub note : String,
45
pub added_by : String,
46
pub created_ms : i64 ,
439ecd87 Andy McCall 11 days ago ↶
47
48
pub archived_ms : Option< i64 > ,
49
50
pub moved_ms : i64 ,
046e3352 Andy McCall 9 days ago ↶
51
52
pub iteration_id : Option< i64 > ,
e1ec9fbf Andy McCall 1 month ago ↶
53
}
54
046e3352 Andy McCall 9 days ago ↶
55
56
57
# [ derive ( Clone, Debug, FromRow ) ]
58
pub struct Iteration {
59
pub id : i64 ,
60
pub project_id : i64 ,
61
pub name : String,
62
pub start_ms : i64 ,
63
64
pub end_ms : i64 ,
65
pub closed_ms : Option< i64 > ,
66
}
67
439ecd87 Andy McCall 11 days ago ↶
68
69
const DAY_MS : i64 = 86_400_000 ;
70
e1ec9fbf Andy McCall 1 month ago ↶
71
pub const DEFAULT_COLUMNS : [ & str ; 3 ] = [ " Backlog" , " Doing" , " Done" ] ;
72
73
pub async fn create ( pool : & Pool, owner : & str , title : & str , description : & str , public : bool ) -> sqlx:: Result< Project> {
941abdd6 Andy McCall 1 month ago ↶
74
let mut tx = super :: begin_write( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
75
let number: i64 =
5d90e0e0 Andy McCall 20 days ago
76
sqlx:: query_scalar( " SELECT COALESCE(MAX(number), 0) + 1 FROM projects WHERE owner = $1" ) .bind ( owner) .fetch_one ( & mut * tx) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
77
let now = now_ms ( ) ;
78
let p: Project = sqlx:: query_as(
5d90e0e0 Andy McCall 20 days ago
79
" INSERT INTO projects (owner, number, title, description, public, created_ms, updated_ms) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *" ,
e1ec9fbf Andy McCall 1 month ago ↶
80
)
81
.bind ( owner)
82
.bind ( number)
83
.bind ( title)
84
.bind ( description)
85
.bind ( public)
86
.bind ( now)
87
.bind ( now)
88
.fetch_one ( & mut * tx)
89
.await? ;
b1e92a62 Andy McCall 26 days ago ↶
90
5d90e0e0 Andy McCall 20 days ago
91
sqlx:: query( " DELETE FROM project_redirects WHERE owner = $1 AND number = $2" ) .bind ( owner) .bind ( number) .execute ( & mut * tx) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
92
for ( i, name) in DEFAULT_COLUMNS .iter ( ) .enumerate ( ) {
1000b40a Andy McCall 10 days ago ↶
93
sqlx:: query( " INSERT INTO project_columns (project_id, name, position, takes_finished) VALUES ($1, $2, $3, $4)" )
e1ec9fbf Andy McCall 1 month ago ↶
94
.bind ( p.id)
95
.bind ( name)
96
.bind ( i as i64 )
1000b40a Andy McCall 10 days ago ↶
97
.bind ( * name = = " Done" )
e1ec9fbf Andy McCall 1 month ago ↶
98
.execute ( & mut * tx)
99
.await? ;
100
}
101
tx.commit ( ) .await? ;
102
Ok ( p)
103
}
104
105
pub async fn of_owner ( pool : & Pool, owner : & str ) -> sqlx:: Result< Vec< Project> > {
5d90e0e0 Andy McCall 20 days ago
106
sqlx:: query_as( " SELECT * FROM projects WHERE owner = $1 ORDER BY closed, updated_ms DESC" ) .bind ( owner) .fetch_all ( pool) .await
e1ec9fbf Andy McCall 1 month ago ↶
107
}
108
b1e92a62 Andy McCall 26 days ago ↶
109
110
111
pub async fn transfer ( pool : & Pool, id : i64 , to : & str ) -> sqlx:: Result< Project> {
112
let mut tx = super :: begin_write( pool) .await? ;
5d90e0e0 Andy McCall 20 days ago
113
let old: Project = sqlx:: query_as( " SELECT * FROM projects WHERE id = $1" ) .bind ( id) .fetch_one ( & mut * tx) .await? ;
b1e92a62 Andy McCall 26 days ago ↶
114
let number: i64 =
5d90e0e0 Andy McCall 20 days ago
115
sqlx:: query_scalar( " SELECT COALESCE(MAX(number), 0) + 1 FROM projects WHERE owner = $1" ) .bind ( to) .fetch_one ( & mut * tx) .await? ;
d1bf2d69 Andy McCall 20 days ago ↶
116
sqlx:: query( " INSERT INTO project_redirects (owner, number, project_id) VALUES ($1, $2, $3) ON CONFLICT (owner, number) DO UPDATE SET project_id = excluded.project_id" )
b1e92a62 Andy McCall 26 days ago ↶
117
.bind ( & old.owner)
118
.bind ( old.number)
119
.bind ( id)
120
.execute ( & mut * tx)
121
.await? ;
5d90e0e0 Andy McCall 20 days ago
122
sqlx:: query( " DELETE FROM project_redirects WHERE owner = $1 AND number = $2" ) .bind ( to) .bind ( number) .execute ( & mut * tx) .await? ;
123
let moved: Project = sqlx:: query_as( " UPDATE projects SET owner = $1, number = $2, updated_ms = $3 WHERE id = $4 RETURNING *" )
b1e92a62 Andy McCall 26 days ago ↶
124
.bind ( to)
125
.bind ( number)
126
.bind ( now_ms ( ) )
127
.bind ( id)
128
.fetch_one ( & mut * tx)
129
.await? ;
130
tx.commit ( ) .await? ;
131
Ok ( moved)
132
}
133
134
135
136
pub async fn redirected ( pool : & Pool, owner : & str , number : i64 ) -> sqlx:: Result< Option< Project> > {
5d90e0e0 Andy McCall 20 days ago
137
sqlx:: query_as( " SELECT p.* FROM project_redirects r JOIN projects p ON p.id = r.project_id WHERE r.owner = $1 AND r.number = $2" )
b1e92a62 Andy McCall 26 days ago ↶
138
.bind ( owner)
139
.bind ( number)
140
.fetch_optional ( pool)
141
.await
142
}
143
e1ec9fbf Andy McCall 1 month ago ↶
144
pub async fn get ( pool : & Pool, owner : & str , number : i64 ) -> sqlx:: Result< Option< Project> > {
5d90e0e0 Andy McCall 20 days ago
145
sqlx:: query_as( " SELECT * FROM projects WHERE owner = $1 AND number = $2" ) .bind ( owner) .bind ( number) .fetch_optional ( pool) .await
e1ec9fbf Andy McCall 1 month ago ↶
146
}
147
148
pub async fn set ( pool : & Pool, id : i64 , title : & str , description : & str , public : bool , closed : bool ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
149
sqlx:: query( " UPDATE projects SET title = $1, description = $2, public = $3, closed = $4, updated_ms = $5 WHERE id = $6" )
e1ec9fbf Andy McCall 1 month ago ↶
150
.bind ( title)
151
.bind ( description)
152
.bind ( public)
153
.bind ( closed)
154
.bind ( now_ms ( ) )
155
.bind ( id)
156
.execute ( pool)
157
.await? ;
158
Ok ( ( ) )
159
}
160
161
pub async fn delete ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
162
sqlx:: query( " DELETE FROM projects WHERE id = $1" ) .bind ( id) .execute ( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
163
Ok ( ( ) )
164
}
165
166
async fn touch ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
167
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now_ms ( ) ) .bind ( id) .execute ( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
168
Ok ( ( ) )
169
}
170
171
pub async fn columns ( pool : & Pool, project_id : i64 ) -> sqlx:: Result< Vec< Column> > {
5d90e0e0 Andy McCall 20 days ago
172
sqlx:: query_as( " SELECT * FROM project_columns WHERE project_id = $1 ORDER BY position, id" ) .bind ( project_id) .fetch_all ( pool) .await
e1ec9fbf Andy McCall 1 month ago ↶
173
}
174
175
pub async fn cards ( pool : & Pool, project_id : i64 ) -> sqlx:: Result< Vec< Card> > {
5d90e0e0 Andy McCall 20 days ago
176
sqlx:: query_as( " SELECT * FROM project_cards WHERE project_id = $1 ORDER BY position, id" ) .bind ( project_id) .fetch_all ( pool) .await
e1ec9fbf Andy McCall 1 month ago ↶
177
}
178
179
pub async fn add_column ( pool : & Pool, project_id : i64 , name : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
180
let pos: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_columns WHERE project_id = $1" )
e1ec9fbf Andy McCall 1 month ago ↶
181
.bind ( project_id)
182
.fetch_one ( pool)
183
.await? ;
5d90e0e0 Andy McCall 20 days ago
184
sqlx:: query( " INSERT INTO project_columns (project_id, name, position) VALUES ($1, $2, $3)" )
e1ec9fbf Andy McCall 1 month ago ↶
185
.bind ( project_id)
186
.bind ( name)
187
.bind ( pos)
188
.execute ( pool)
189
.await? ;
190
touch ( pool, project_id) .await
191
}
192
193
pub async fn rename_column ( pool : & Pool, project_id : i64 , column_id : i64 , name : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
194
sqlx:: query( " UPDATE project_columns SET name = $1 WHERE id = $2 AND project_id = $3" )
e1ec9fbf Andy McCall 1 month ago ↶
195
.bind ( name)
196
.bind ( column_id)
197
.bind ( project_id)
198
.execute ( pool)
199
.await? ;
200
touch ( pool, project_id) .await
201
}
202
203
204
pub async fn shift_column ( pool : & Pool, project_id : i64 , column_id : i64 , by : i64 ) -> sqlx:: Result< ( ) > {
205
let cols = columns ( pool, project_id) .await? ;
206
let Some ( i) = cols.iter ( ) .position ( | c | c.id = = column_id ) else { return Ok ( ( ) ) } ;
207
let j = i as i64 + by;
208
if j < 0 | | j as usize > = cols.len ( ) {
209
return Ok ( ( ) ) ;
210
}
211
let mut ids: Vec< i64 > = cols.iter ( ) .map ( | c | c.id ) .collect ( ) ;
212
ids.swap ( i, j as usize ) ;
213
for ( pos, id) in ids.iter ( ) .enumerate ( ) {
5d90e0e0 Andy McCall 20 days ago
214
sqlx:: query( " UPDATE project_columns SET position = $1 WHERE id = $2" ) .bind ( pos as i64 ) .bind ( id) .execute ( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
215
}
216
touch ( pool, project_id) .await
217
}
218
306aec18 Andy McCall 2 days ago ↶
219
220
pub async fn place_column ( pool : & Pool, project_id : i64 , column_id : i64 , position : i64 ) -> sqlx:: Result< ( ) > {
221
let cols = columns ( pool, project_id) .await? ;
222
let Some ( i) = cols.iter ( ) .position ( | c | c.id = = column_id ) else { return Ok ( ( ) ) } ;
223
let mut ids: Vec< i64 > = cols.iter ( ) .map ( | c | c.id ) .collect ( ) ;
224
let id = ids.remove ( i) ;
225
ids.insert ( ( position.max ( 0 ) as usize ) .min ( ids.len ( ) ) , id) ;
226
for ( pos, id) in ids.iter ( ) .enumerate ( ) {
227
sqlx:: query( " UPDATE project_columns SET position = $1 WHERE id = $2" ) .bind ( pos as i64 ) .bind ( id) .execute ( pool) .await? ;
228
}
229
touch ( pool, project_id) .await
230
}
231
e1ec9fbf Andy McCall 1 month ago ↶
232
233
pub async fn remove_column ( pool : & Pool, project_id : i64 , column_id : i64 ) -> sqlx:: Result< bool > {
234
let cols = columns ( pool, project_id) .await? ;
235
let Some ( to) = cols.iter ( ) .find ( | c | c.id ! = column_id ) else { return Ok ( false ) } ;
236
if ! cols.iter ( ) .any ( | c | c.id = = column_id ) {
237
return Ok ( false ) ;
238
}
941abdd6 Andy McCall 1 month ago ↶
239
let mut tx = super :: begin_write( pool) .await? ;
5d90e0e0 Andy McCall 20 days ago
240
let base: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = $1" )
e1ec9fbf Andy McCall 1 month ago ↶
241
.bind ( to.id)
242
.fetch_one ( & mut * tx)
243
.await? ;
0bc95135 Andy McCall 2 days ago ↶
244
log_moves ( & mut * tx, Some ( to.id) , now_ms ( ) , " column_id = $1 AND archived_ms IS NULL" , & [ column_id] ) .await? ;
5d90e0e0 Andy McCall 20 days ago
245
sqlx:: query( " UPDATE project_cards SET column_id = $1, position = position + $2 WHERE column_id = $3" )
e1ec9fbf Andy McCall 1 month ago ↶
246
.bind ( to.id)
247
.bind ( base)
248
.bind ( column_id)
249
.execute ( & mut * tx)
250
.await? ;
5d90e0e0 Andy McCall 20 days ago
251
sqlx:: query( " DELETE FROM project_columns WHERE id = $1" ) .bind ( column_id) .execute ( & mut * tx) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
252
tx.commit ( ) .await? ;
253
touch ( pool, project_id) .await? ;
254
Ok ( true )
255
}
256
0bc95135 Andy McCall 2 days ago ↶
257
258
259
260
261
async fn log_moves < 'e , E: sqlx:: Executor< 'e , Database = super:: Db> > (
262
ex : E,
263
column : Option< i64 > ,
264
at : i64 ,
265
select : & str ,
266
binds : & [i64 ],
267
) -> sqlx:: Result< ( ) > {
268
let sql = format! (
269
" INSERT INTO project_card_moves (project_id, card_id, column_id, at_ms) SELECT project_id, id, {} , {at} FROM project_cards WHERE {select} " ,
270
match column {
271
Some ( c) => c.to_string ( ) ,
272
None => " NULL" .to_string ( ) ,
273
}
274
) ;
275
let mut q = sqlx:: query( & sql) ;
276
for b in binds {
277
q = q.bind ( * b) ;
278
}
279
q.execute ( ex) .await? ;
280
Ok ( ( ) )
281
}
282
283
284
285
pub async fn moves ( pool : & Pool, project_id : i64 , since : i64 ) -> sqlx:: Result< Vec< ( i64 , Option< i64 > , i64 ) > > {
286
sqlx:: query_as( " SELECT card_id, column_id, at_ms FROM project_card_moves WHERE project_id = $1 AND at_ms >= $2 ORDER BY at_ms, id" )
287
.bind ( project_id)
288
.bind ( since)
289
.fetch_all ( pool)
290
.await
291
}
292
e1ec9fbf Andy McCall 1 month ago ↶
293
pub struct NewCard < 'a > {
294
pub column_id : i64 ,
295
pub kind : & 'a str ,
296
pub repo_id : Option< i64 > ,
297
pub number : Option< i64 > ,
298
pub note : & 'a str ,
299
pub added_by : & 'a str ,
300
}
301
302
303
304
pub async fn add_card ( pool : & Pool, project_id : i64 , c : NewCard< '_ > ) -> sqlx:: Result< bool > {
5d90e0e0 Andy McCall 20 days ago
305
let pos: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = $1" )
e1ec9fbf Andy McCall 1 month ago ↶
306
.bind ( c.column_id)
307
.fetch_one ( pool)
308
.await? ;
0bc95135 Andy McCall 2 days ago ↶
309
let now = now_ms ( ) ;
310
let id: Option< i64 > = sqlx:: query_scalar(
311
" INSERT INTO project_cards (project_id, column_id, position, kind, repo_id, number, note, added_by, created_ms, moved_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9) ON CONFLICT DO NOTHING RETURNING id" ,
e1ec9fbf Andy McCall 1 month ago ↶
312
)
313
.bind ( project_id)
314
.bind ( c.column_id)
315
.bind ( pos)
316
.bind ( c.kind)
317
.bind ( c.repo_id)
318
.bind ( c.number)
319
.bind ( c.note)
320
.bind ( c.added_by)
0bc95135 Andy McCall 2 days ago ↶
321
.bind ( now)
322
.fetch_optional ( pool)
e1ec9fbf Andy McCall 1 month ago ↶
323
.await? ;
0bc95135 Andy McCall 2 days ago ↶
324
if let Some ( id) = id {
325
log_moves ( pool, Some ( c.column_id) , now, " id = $1" , & [ id] ) .await? ;
326
}
e1ec9fbf Andy McCall 1 month ago ↶
327
touch ( pool, project_id) .await? ;
0bc95135 Andy McCall 2 days ago ↶
328
Ok ( id.is_some ( ) )
e1ec9fbf Andy McCall 1 month ago ↶
329
}
330
331
332
pub async fn move_card ( pool : & Pool, project_id : i64 , card_id : i64 , column_id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
333
let ok: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM project_columns WHERE id = $1 AND project_id = $2" )
e1ec9fbf Andy McCall 1 month ago ↶
334
.bind ( column_id)
335
.bind ( project_id)
336
.fetch_one ( pool)
337
.await? ;
338
if ok = = 0 {
339
return Ok ( ( ) ) ;
340
}
5d90e0e0 Andy McCall 20 days ago
341
let pos: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = $1" )
e1ec9fbf Andy McCall 1 month ago ↶
342
.bind ( column_id)
343
.fetch_one ( pool)
344
.await? ;
0bc95135 Andy McCall 2 days ago ↶
345
let now = now_ms ( ) ;
346
log_moves (
347
pool,
348
Some ( column_id) ,
349
now,
350
" id = $1 AND project_id = $2 AND column_id <> $3 AND archived_ms IS NULL" ,
351
& [ card_id, project_id, column_id] ,
352
)
353
.await? ;
439ecd87 Andy McCall 11 days ago ↶
354
sqlx:: query( " UPDATE project_cards SET moved_ms = CASE WHEN column_id = $1 THEN moved_ms ELSE $3 END, column_id = $1, position = $2 WHERE id = $4 AND project_id = $5" )
e1ec9fbf Andy McCall 1 month ago ↶
355
.bind ( column_id)
356
.bind ( pos)
0bc95135 Andy McCall 2 days ago ↶
357
.bind ( now)
e1ec9fbf Andy McCall 1 month ago ↶
358
.bind ( card_id)
359
.bind ( project_id)
360
.execute ( pool)
361
.await? ;
362
touch ( pool, project_id) .await
363
}
364
456f4197 Andy McCall 12 days ago ↶
365
366
367
pub async fn move_cards ( pool : & Pool, project_id : i64 , card_ids : & [i64 ], column_id : i64 ) -> sqlx:: Result< ( ) > {
368
let mut tx = pool.begin ( ) .await? ;
369
let pos: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = $1" )
370
.bind ( column_id)
371
.fetch_one ( & mut * tx)
372
.await? ;
439ecd87 Andy McCall 11 days ago ↶
373
let now = now_ms ( ) ;
456f4197 Andy McCall 12 days ago ↶
374
for ( i, id) in card_ids.iter ( ) .enumerate ( ) {
0bc95135 Andy McCall 2 days ago ↶
375
log_moves (
376
& mut * tx,
377
Some ( column_id) ,
378
now,
379
" id = $1 AND project_id = $2 AND column_id <> $3 AND archived_ms IS NULL" ,
380
& [ * id, project_id, column_id] ,
381
)
382
.await? ;
439ecd87 Andy McCall 11 days ago ↶
383
sqlx:: query( " UPDATE project_cards SET column_id = $1, position = $2, moved_ms = $3 WHERE id = $4 AND project_id = $5" )
456f4197 Andy McCall 12 days ago ↶
384
.bind ( column_id)
385
.bind ( pos + i as i64 )
439ecd87 Andy McCall 11 days ago ↶
386
.bind ( now)
456f4197 Andy McCall 12 days ago ↶
387
.bind ( id)
388
.bind ( project_id)
389
.execute ( & mut * tx)
390
.await? ;
391
}
439ecd87 Andy McCall 11 days ago ↶
392
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now) .bind ( project_id) .execute ( & mut * tx) .await? ;
456f4197 Andy McCall 12 days ago ↶
393
tx.commit ( ) .await
439ecd87 Andy McCall 11 days ago ↶
394
}
395
396
397
pub async fn archive_cards ( pool : & Pool, project_id : i64 , card_ids : & [i64 ]) -> sqlx:: Result< u64 > {
398
let mut tx = pool.begin ( ) .await? ;
399
let now = now_ms ( ) ;
400
let mut n = 0 ;
401
for id in card_ids {
0bc95135 Andy McCall 2 days ago ↶
402
log_moves ( & mut * tx, None , now, " id = $1 AND project_id = $2 AND archived_ms IS NULL" , & [ * id, project_id] ) .await? ;
439ecd87 Andy McCall 11 days ago ↶
403
n + = sqlx:: query( " UPDATE project_cards SET archived_ms = $1 WHERE id = $2 AND project_id = $3 AND archived_ms IS NULL" )
404
.bind ( now)
405
.bind ( id)
406
.bind ( project_id)
407
.execute ( & mut * tx)
408
.await?
409
.rows_affected ( ) ;
410
}
411
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now) .bind ( project_id) .execute ( & mut * tx) .await? ;
412
tx.commit ( ) .await? ;
413
Ok ( n)
414
}
415
416
417
418
419
pub async fn restore_card ( pool : & Pool, project_id : i64 , card_id : i64 ) -> sqlx:: Result< bool > {
420
let mut tx = pool.begin ( ) .await? ;
421
let pos: i64 = sqlx:: query_scalar(
422
" SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = (SELECT column_id FROM project_cards WHERE id = $1)" ,
423
)
424
.bind ( card_id)
425
.fetch_one ( & mut * tx)
426
.await? ;
427
let now = now_ms ( ) ;
0bc95135 Andy McCall 2 days ago ↶
428
sqlx:: query( & format! (
429
" INSERT INTO project_card_moves (project_id, card_id, column_id, at_ms) SELECT project_id, id, column_id, {now} FROM project_cards \
430
WHERE id = $1 AND project_id = $2 AND archived_ms IS NOT NULL"
431
) )
432
.bind ( card_id)
433
.bind ( project_id)
434
.execute ( & mut * tx)
435
.await? ;
046e3352 Andy McCall 9 days ago ↶
436
439ecd87 Andy McCall 11 days ago ↶
437
let r = sqlx:: query(
046e3352 Andy McCall 9 days ago ↶
438
" UPDATE project_cards SET archived_ms = NULL, iteration_id = NULL, moved_ms = $1, position = $2 \
439
WHERE id = $3 AND project_id = $4 AND archived_ms IS NOT NULL" ,
439ecd87 Andy McCall 11 days ago ↶
440
)
441
.bind ( now)
442
.bind ( pos)
443
.bind ( card_id)
444
.bind ( project_id)
445
.execute ( & mut * tx)
446
.await? ;
447
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now) .bind ( project_id) .execute ( & mut * tx) .await? ;
448
tx.commit ( ) .await? ;
449
Ok ( r.rows_affected ( ) = = 1 )
450
}
451
452
453
pub fn older_than ( card : & Card, days : i64 , now : i64 ) -> bool {
454
card.moved_ms < now - days * DAY_MS
046e3352 Andy McCall 9 days ago ↶
455
}
456
457
458
pub async fn iterations ( pool : & Pool, project_id : i64 ) -> sqlx:: Result< Vec< Iteration> > {
459
sqlx:: query_as( " SELECT * FROM project_iterations WHERE project_id = $1 ORDER BY start_ms DESC, id DESC" )
460
.bind ( project_id)
461
.fetch_all ( pool)
462
.await
463
}
464
465
466
pub async fn start_iteration ( pool : & Pool, project_id : i64 , name : & str , start_ms : i64 , end_ms : i64 ) -> sqlx:: Result< Option< Iteration> > {
467
let mut tx = super :: begin_write( pool) .await? ;
468
let open: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM project_iterations WHERE project_id = $1 AND closed_ms IS NULL" )
469
.bind ( project_id)
470
.fetch_one ( & mut * tx)
471
.await? ;
472
if open > 0 {
473
return Ok ( None ) ;
474
}
475
let it: Iteration =
476
sqlx:: query_as( " INSERT INTO project_iterations (project_id, name, start_ms, end_ms) VALUES ($1, $2, $3, $4) RETURNING *" )
477
.bind ( project_id)
478
.bind ( name)
479
.bind ( start_ms)
480
.bind ( end_ms)
481
.fetch_one ( & mut * tx)
482
.await? ;
483
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now_ms ( ) ) .bind ( project_id) .execute ( & mut * tx) .await? ;
484
tx.commit ( ) .await? ;
485
Ok ( Some ( it) )
486
}
487
488
489
490
pub fn done_column ( columns : & [Column]) -> Option< i64 > {
491
columns.iter ( ) .find ( | c | c.takes_finished ) .or ( columns.last ( ) ) .map ( | c | c.id )
492
}
493
494
495
496
pub async fn close_iteration ( pool : & Pool, project_id : i64 , iteration_id : i64 , done : Option< i64 > ) -> sqlx:: Result< Option< u64 > > {
497
let mut tx = super :: begin_write( pool) .await? ;
498
let now = now_ms ( ) ;
499
let closed = sqlx:: query( " UPDATE project_iterations SET closed_ms = $1 WHERE id = $2 AND project_id = $3 AND closed_ms IS NULL" )
500
.bind ( now)
501
.bind ( iteration_id)
502
.bind ( project_id)
503
.execute ( & mut * tx)
504
.await? ;
505
if closed.rows_affected ( ) = = 0 {
506
return Ok ( None ) ;
507
}
508
let mut n = 0 ;
509
if let Some ( done) = done {
0bc95135 Andy McCall 2 days ago ↶
510
log_moves ( & mut * tx, None , now, " project_id = $1 AND column_id = $2 AND archived_ms IS NULL" , & [ project_id, done] ) .await? ;
046e3352 Andy McCall 9 days ago ↶
511
n = sqlx:: query(
512
" UPDATE project_cards SET archived_ms = $1, iteration_id = $2 WHERE project_id = $3 AND column_id = $4 AND archived_ms IS NULL" ,
513
)
514
.bind ( now)
515
.bind ( iteration_id)
516
.bind ( project_id)
517
.bind ( done)
518
.execute ( & mut * tx)
519
.await?
520
.rows_affected ( ) ;
521
}
522
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now) .bind ( project_id) .execute ( & mut * tx) .await? ;
523
tx.commit ( ) .await? ;
524
Ok ( Some ( n) )
525
}
526
527
528
529
530
pub async fn done_times ( pool : & Pool, it : & Iteration, done : Option< i64 > ) -> sqlx:: Result< Vec< i64 > > {
531
sqlx:: query_scalar(
532
" SELECT moved_ms FROM project_cards WHERE project_id = $1 AND (iteration_id = $2 \
533
OR ($3 AND iteration_id IS NULL AND column_id = $4 AND (archived_ms IS NULL OR archived_ms >= $5))) ORDER BY moved_ms" ,
534
)
535
.bind ( it.project_id)
536
.bind ( it.id)
537
.bind ( it.closed_ms.is_none ( ) )
538
.bind ( done.unwrap_or ( - 1 ) )
539
.bind ( it.start_ms)
540
.fetch_all ( pool)
541
.await
542
}
543
544
545
546
547
pub fn burn_up ( times : & [i64 ], start_ms : i64 , end_ms : i64 , now : i64 ) -> Vec< usize > {
548
let days = ( ( end_ms - start_ms + DAY_MS - 1 ) / DAY_MS ) .max ( 1 ) ;
549
let upto = ( ( now.min ( end_ms - 1 ) - start_ms) / DAY_MS + 1 ) .clamp ( 0 , days) ;
550
( 0 .. upto) .map ( | d | times.iter ( ) .filter ( | & & t | t < start_ms + ( d + 1 ) * DAY_MS ) .count ( ) ) .collect ( )
551
}
552
553
554
pub fn iteration_days ( it : & Iteration) -> i64 {
555
( ( it.end_ms - it.start_ms + DAY_MS - 1 ) / DAY_MS ) .max ( 1 )
439ecd87 Andy McCall 11 days ago ↶
556
}
557
946fe7a0 Andy McCall 11 days ago ↶
558
559
560
439ecd87 Andy McCall 11 days ago ↶
561
pub async fn set_archive_after ( pool : & Pool, project_id : i64 , column_id : i64 , days : Option< i64 > ) -> sqlx:: Result< ( ) > {
562
sqlx:: query( " UPDATE project_columns SET archive_after_days = $1 WHERE id = $2 AND project_id = $3" )
563
.bind ( days)
564
.bind ( column_id)
565
.bind ( project_id)
566
.execute ( pool)
567
.await? ;
568
touch ( pool, project_id) .await
569
}
570
946fe7a0 Andy McCall 11 days ago ↶
571
572
573
pub const FINISHED_KEPT_DAYS : i64 = 14 ;
0bc95135 Andy McCall 2 days ago ↶
574
575
576
577
578
const FINISHED_DUE : & str = " archived_ms IS NULL AND moved_ms < $1 \
579
AND EXISTS (SELECT 1 FROM project_columns c WHERE c.id = project_cards.column_id AND c.archive_after_days IS NULL) \
580
AND ((kind = 'issue' AND EXISTS (SELECT 1 FROM issues i WHERE i.repo_id = project_cards.repo_id \
581
AND i.number = project_cards.number AND i.state = 'closed')) \
582
OR (kind = 'proposal' AND EXISTS (SELECT 1 FROM proposals p WHERE p.repo_id = project_cards.repo_id \
583
AND p.number = project_cards.number AND p.state IN ('closed', 'landed'))))" ;
946fe7a0 Andy McCall 11 days ago ↶
584
585
586
pub const NEVER : i64 = - 1 ;
587
588
589
590
591
592
439ecd87 Andy McCall 11 days ago ↶
593
pub async fn archive_due ( pool : & Pool, now : i64 ) -> sqlx:: Result< u64 > {
0bc95135 Andy McCall 2 days ago ↶
594
log_moves (
595
pool,
596
None ,
597
now,
598
" archived_ms IS NULL AND EXISTS (SELECT 1 FROM project_columns c WHERE c.id = project_cards.column_id \
599
AND c.archive_after_days >= 0 AND project_cards.moved_ms < $1 - c.archive_after_days * $2)" ,
600
& [ now, DAY_MS ] ,
601
)
602
.await? ;
946fe7a0 Andy McCall 11 days ago ↶
603
let set = sqlx:: query(
439ecd87 Andy McCall 11 days ago ↶
604
" UPDATE project_cards SET archived_ms = $1 WHERE archived_ms IS NULL AND EXISTS \
946fe7a0 Andy McCall 11 days ago ↶
605
(SELECT 1 FROM project_columns c WHERE c.id = project_cards.column_id AND c.archive_after_days >= 0 \
439ecd87 Andy McCall 11 days ago ↶
606
AND project_cards.moved_ms < $1 - c.archive_after_days * $2)" ,
607
)
608
.bind ( now)
609
.bind ( DAY_MS )
946fe7a0 Andy McCall 11 days ago ↶
610
.execute ( pool)
611
.await? ;
0bc95135 Andy McCall 2 days ago ↶
612
log_moves ( pool, None , now, FINISHED_DUE , & [ now - FINISHED_KEPT_DAYS * DAY_MS ] ) .await? ;
613
let finished = sqlx:: query( & format! ( " UPDATE project_cards SET archived_ms = $2 WHERE {FINISHED_DUE} " ) )
614
.bind ( now - FINISHED_KEPT_DAYS * DAY_MS )
615
.bind ( now)
616
.execute ( pool)
617
.await? ;
946fe7a0 Andy McCall 11 days ago ↶
618
Ok ( set.rows_affected ( ) + finished.rows_affected ( ) )
456f4197 Andy McCall 12 days ago ↶
619
}
620
e1ec9fbf Andy McCall 1 month ago ↶
621
622
pub async fn shift_card ( pool : & Pool, project_id : i64 , card_id : i64 , by : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
623
let Some ( card) : Option< Card> = sqlx:: query_as( " SELECT * FROM project_cards WHERE id = $1 AND project_id = $2" )
e1ec9fbf Andy McCall 1 month ago ↶
624
.bind ( card_id)
625
.bind ( project_id)
626
.fetch_optional ( pool)
627
.await?
628
else {
629
return Ok ( ( ) ) ;
630
} ;
5d90e0e0 Andy McCall 20 days ago
631
let col: Vec< Card> = sqlx:: query_as( " SELECT * FROM project_cards WHERE column_id = $1 ORDER BY position, id" )
e1ec9fbf Andy McCall 1 month ago ↶
632
.bind ( card.column_id)
633
.fetch_all ( pool)
634
.await? ;
635
let Some ( i) = col.iter ( ) .position ( | c | c.id = = card_id ) else { return Ok ( ( ) ) } ;
636
let j = i as i64 + by;
637
if j < 0 | | j as usize > = col.len ( ) {
638
return Ok ( ( ) ) ;
639
}
640
let mut ids: Vec< i64 > = col.iter ( ) .map ( | c | c.id ) .collect ( ) ;
641
ids.swap ( i, j as usize ) ;
642
for ( pos, id) in ids.iter ( ) .enumerate ( ) {
5d90e0e0 Andy McCall 20 days ago
643
sqlx:: query( " UPDATE project_cards SET position = $1 WHERE id = $2" ) .bind ( pos as i64 ) .bind ( id) .execute ( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
644
}
645
touch ( pool, project_id) .await
646
}
647
648
pub async fn remove_card ( pool : & Pool, project_id : i64 , card_id : i64 ) -> sqlx:: Result< ( ) > {
0bc95135 Andy McCall 2 days ago ↶
649
log_moves ( pool, None , now_ms ( ) , " id = $1 AND project_id = $2 AND archived_ms IS NULL" , & [ card_id, project_id] ) .await? ;
5d90e0e0 Andy McCall 20 days ago
650
sqlx:: query( " DELETE FROM project_cards WHERE id = $1 AND project_id = $2" ) .bind ( card_id) .bind ( project_id) .execute ( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
651
touch ( pool, project_id) .await
652
}
653
654
1000b40a Andy McCall 10 days ago ↶
655
656
pub async fn set_takes_finished ( pool : & Pool, project_id : i64 , column_id : Option< i64 > ) -> sqlx:: Result< ( ) > {
657
let mut tx = super :: begin_write( pool) .await? ;
658
sqlx:: query( " UPDATE project_columns SET takes_finished = FALSE WHERE project_id = $1" ) .bind ( project_id) .execute ( & mut * tx) .await? ;
659
if let Some ( c) = column_id {
660
sqlx:: query( " UPDATE project_columns SET takes_finished = TRUE WHERE id = $1 AND project_id = $2" )
661
.bind ( c)
662
.bind ( project_id)
663
.execute ( & mut * tx)
664
.await? ;
665
}
666
tx.commit ( ) .await? ;
667
touch ( pool, project_id) .await
668
}
669
670
671
672
673
pub async fn finished ( pool : & Pool, repo_id : i64 , number : i64 ) -> sqlx:: Result< ( ) > {
674
let cards: Vec< ( i64 , i64 , i64 ) > = sqlx:: query_as(
675
" SELECT c.id, c.project_id, c.column_id FROM project_cards c
676
WHERE c.repo_id = $1 AND c.number = $2 AND c.kind <> 'note' AND c.archived_ms IS NULL" ,
677
)
678
.bind ( repo_id)
679
.bind ( number)
680
.fetch_all ( pool)
681
.await? ;
682
for ( card, project, column) in cards {
683
let to: Option< i64 > =
684
sqlx:: query_scalar( " SELECT id FROM project_columns WHERE project_id = $1 AND takes_finished ORDER BY position LIMIT 1" )
685
.bind ( project)
686
.fetch_optional ( pool)
687
.await? ;
688
if let Some ( to) = to.filter ( | to | * to ! = column) {
689
move_card ( pool, project, card, to) .await? ;
690
}
691
}
692
Ok ( ( ) )
693
}
694
e1ec9fbf Andy McCall 1 month ago ↶
695
pub async fn placements ( pool : & Pool, repo_id : i64 , number : i64 ) -> sqlx:: Result< Vec< ( Project, String) > > {
439ecd87 Andy McCall 11 days ago ↶
696
let rows: Vec< ( i64 , String, Option< i64 > ) > = sqlx:: query_as(
697
" SELECT c.project_id, col.name, c.archived_ms FROM project_cards c JOIN project_columns col ON col.id = c.column_id WHERE c.repo_id = $1 AND c.number = $2 AND c.kind <> 'note'" ,
e1ec9fbf Andy McCall 1 month ago ↶
698
)
699
.bind ( repo_id)
700
.bind ( number)
701
.fetch_all ( pool)
702
.await? ;
703
let mut out = Vec :: new( ) ;
439ecd87 Andy McCall 11 days ago ↶
704
for ( pid, col, archived) in rows {
705
let col = if archived.is_some ( ) { format! ( " {col} , archived" ) } else { col } ;
5d90e0e0 Andy McCall 20 days ago
706
if let Some ( p) = sqlx:: query_as:: < _ , Project> ( " SELECT * FROM projects WHERE id = $1" ) .bind ( pid) .fetch_optional ( pool) .await? {
e1ec9fbf Andy McCall 1 month ago ↶
707
out.push ( ( p, col) ) ;
708
}
709
}
710
Ok ( out)
0bc95135 Andy McCall 2 days ago ↶
711
}
712
713
714
# [ derive ( Clone, Debug, FromRow ) ]
715
pub struct View {
716
pub id : i64 ,
717
pub project_id : i64 ,
718
pub name : String,
719
720
pub layout : String,
721
pub q : String,
722
723
724
pub lane : String,
725
726
727
pub sort : String,
728
pub position : i64 ,
729
pub created_ms : i64 ,
730
}
731
732
pub async fn views ( pool : & Pool, project_id : i64 ) -> sqlx:: Result< Vec< View> > {
733
sqlx:: query_as( " SELECT * FROM project_views WHERE project_id = $1 ORDER BY position, id" ) .bind ( project_id) .fetch_all ( pool) .await
734
}
735
736
737
pub async fn save_view ( pool : & Pool, project_id : i64 , name : & str , layout : & str , q : & str , lane : & str , sort : & str ) -> sqlx:: Result< View> {
738
let mut tx = super :: begin_write( pool) .await? ;
739
let existing: Option< i64 > = sqlx:: query_scalar( " SELECT id FROM project_views WHERE project_id = $1 AND name = $2" )
740
.bind ( project_id)
741
.bind ( name)
742
.fetch_optional ( & mut * tx)
743
.await? ;
744
let v: View = match existing {
745
Some ( id) => {
746
sqlx:: query_as( " UPDATE project_views SET layout = $1, q = $2, lane = $3, sort = $4 WHERE id = $5 RETURNING *" )
747
.bind ( layout)
748
.bind ( q)
749
.bind ( lane)
750
.bind ( sort)
751
.bind ( id)
752
.fetch_one ( & mut * tx)
753
.await?
754
}
755
None => {
756
let pos: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_views WHERE project_id = $1" )
757
.bind ( project_id)
758
.fetch_one ( & mut * tx)
759
.await? ;
760
sqlx:: query_as(
761
" INSERT INTO project_views (project_id, name, layout, q, lane, sort, position, created_ms) \
762
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *" ,
763
)
764
.bind ( project_id)
765
.bind ( name)
766
.bind ( layout)
767
.bind ( q)
768
.bind ( lane)
769
.bind ( sort)
770
.bind ( pos)
771
.bind ( now_ms ( ) )
772
.fetch_one ( & mut * tx)
773
.await?
774
}
775
} ;
776
tx.commit ( ) .await? ;
777
touch ( pool, project_id) .await? ;
778
Ok ( v)
779
}
780
781
pub async fn delete_view ( pool : & Pool, project_id : i64 , view_id : i64 ) -> sqlx:: Result< bool > {
782
let r = sqlx:: query( " DELETE FROM project_views WHERE id = $1 AND project_id = $2" ) .bind ( view_id) .bind ( project_id) .execute ( pool) .await? ;
783
touch ( pool, project_id) .await? ;
784
Ok ( r.rows_affected ( ) = = 1 )
785
}
786
787
788
pub async fn set_wip_limit ( pool : & Pool, project_id : i64 , column_id : i64 , limit : Option< i64 > ) -> sqlx:: Result< ( ) > {
789
sqlx:: query( " UPDATE project_columns SET wip_limit = $1 WHERE id = $2 AND project_id = $3" )
790
.bind ( limit)
791
.bind ( column_id)
792
.bind ( project_id)
793
.execute ( pool)
794
.await? ;
795
touch ( pool, project_id) .await
e1ec9fbf Andy McCall 1 month ago ↶
796
}