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 ,
e1ec9fbf Andy McCall 1 month ago ↶
30
}
31
32
# [ derive ( Clone, Debug, FromRow ) ]
33
pub struct Card {
34
pub id : i64 ,
35
pub project_id : i64 ,
36
pub column_id : i64 ,
37
pub position : i64 ,
38
pub kind : String,
39
pub repo_id : Option< i64 > ,
40
pub number : Option< i64 > ,
41
pub note : String,
42
pub added_by : String,
43
pub created_ms : i64 ,
439ecd87 Andy McCall 11 days ago ↶
44
45
pub archived_ms : Option< i64 > ,
46
47
pub moved_ms : i64 ,
046e3352 Andy McCall 8 days ago ↶
48
49
pub iteration_id : Option< i64 > ,
e1ec9fbf Andy McCall 1 month ago ↶
50
}
51
046e3352 Andy McCall 8 days ago ↶
52
53
54
# [ derive ( Clone, Debug, FromRow ) ]
55
pub struct Iteration {
56
pub id : i64 ,
57
pub project_id : i64 ,
58
pub name : String,
59
pub start_ms : i64 ,
60
61
pub end_ms : i64 ,
62
pub closed_ms : Option< i64 > ,
63
}
64
439ecd87 Andy McCall 11 days ago ↶
65
66
const DAY_MS : i64 = 86_400_000 ;
67
e1ec9fbf Andy McCall 1 month ago ↶
68
pub const DEFAULT_COLUMNS : [ & str ; 3 ] = [ " Backlog" , " Doing" , " Done" ] ;
69
70
pub async fn create ( pool : & Pool, owner : & str , title : & str , description : & str , public : bool ) -> sqlx:: Result< Project> {
941abdd6 Andy McCall 1 month ago ↶
71
let mut tx = super :: begin_write( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
72
let number: i64 =
5d90e0e0 Andy McCall 19 days ago
73
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 ↶
74
let now = now_ms ( ) ;
75
let p: Project = sqlx:: query_as(
5d90e0e0 Andy McCall 19 days ago
76
" 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 ↶
77
)
78
.bind ( owner)
79
.bind ( number)
80
.bind ( title)
81
.bind ( description)
82
.bind ( public)
83
.bind ( now)
84
.bind ( now)
85
.fetch_one ( & mut * tx)
86
.await? ;
b1e92a62 Andy McCall 25 days ago ↶
87
5d90e0e0 Andy McCall 19 days ago
88
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 ↶
89
for ( i, name) in DEFAULT_COLUMNS .iter ( ) .enumerate ( ) {
1000b40a Andy McCall 10 days ago ↶
90
sqlx:: query( " INSERT INTO project_columns (project_id, name, position, takes_finished) VALUES ($1, $2, $3, $4)" )
e1ec9fbf Andy McCall 1 month ago ↶
91
.bind ( p.id)
92
.bind ( name)
93
.bind ( i as i64 )
1000b40a Andy McCall 10 days ago ↶
94
.bind ( * name = = " Done" )
e1ec9fbf Andy McCall 1 month ago ↶
95
.execute ( & mut * tx)
96
.await? ;
97
}
98
tx.commit ( ) .await? ;
99
Ok ( p)
100
}
101
102
pub async fn of_owner ( pool : & Pool, owner : & str ) -> sqlx:: Result< Vec< Project> > {
5d90e0e0 Andy McCall 19 days ago
103
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 ↶
104
}
105
b1e92a62 Andy McCall 25 days ago ↶
106
107
108
pub async fn transfer ( pool : & Pool, id : i64 , to : & str ) -> sqlx:: Result< Project> {
109
let mut tx = super :: begin_write( pool) .await? ;
5d90e0e0 Andy McCall 19 days ago
110
let old: Project = sqlx:: query_as( " SELECT * FROM projects WHERE id = $1" ) .bind ( id) .fetch_one ( & mut * tx) .await? ;
b1e92a62 Andy McCall 25 days ago ↶
111
let number: i64 =
5d90e0e0 Andy McCall 19 days ago
112
sqlx:: query_scalar( " SELECT COALESCE(MAX(number), 0) + 1 FROM projects WHERE owner = $1" ) .bind ( to) .fetch_one ( & mut * tx) .await? ;
d1bf2d69 Andy McCall 19 days ago ↶
113
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 25 days ago ↶
114
.bind ( & old.owner)
115
.bind ( old.number)
116
.bind ( id)
117
.execute ( & mut * tx)
118
.await? ;
5d90e0e0 Andy McCall 19 days ago
119
sqlx:: query( " DELETE FROM project_redirects WHERE owner = $1 AND number = $2" ) .bind ( to) .bind ( number) .execute ( & mut * tx) .await? ;
120
let moved: Project = sqlx:: query_as( " UPDATE projects SET owner = $1, number = $2, updated_ms = $3 WHERE id = $4 RETURNING *" )
b1e92a62 Andy McCall 25 days ago ↶
121
.bind ( to)
122
.bind ( number)
123
.bind ( now_ms ( ) )
124
.bind ( id)
125
.fetch_one ( & mut * tx)
126
.await? ;
127
tx.commit ( ) .await? ;
128
Ok ( moved)
129
}
130
131
132
133
pub async fn redirected ( pool : & Pool, owner : & str , number : i64 ) -> sqlx:: Result< Option< Project> > {
5d90e0e0 Andy McCall 19 days ago
134
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 25 days ago ↶
135
.bind ( owner)
136
.bind ( number)
137
.fetch_optional ( pool)
138
.await
139
}
140
e1ec9fbf Andy McCall 1 month ago ↶
141
pub async fn get ( pool : & Pool, owner : & str , number : i64 ) -> sqlx:: Result< Option< Project> > {
5d90e0e0 Andy McCall 19 days ago
142
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 ↶
143
}
144
145
pub async fn set ( pool : & Pool, id : i64 , title : & str , description : & str , public : bool , closed : bool ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
146
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 ↶
147
.bind ( title)
148
.bind ( description)
149
.bind ( public)
150
.bind ( closed)
151
.bind ( now_ms ( ) )
152
.bind ( id)
153
.execute ( pool)
154
.await? ;
155
Ok ( ( ) )
156
}
157
158
pub async fn delete ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
159
sqlx:: query( " DELETE FROM projects WHERE id = $1" ) .bind ( id) .execute ( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
160
Ok ( ( ) )
161
}
162
163
async fn touch ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
164
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 ↶
165
Ok ( ( ) )
166
}
167
168
pub async fn columns ( pool : & Pool, project_id : i64 ) -> sqlx:: Result< Vec< Column> > {
5d90e0e0 Andy McCall 19 days ago
169
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 ↶
170
}
171
172
pub async fn cards ( pool : & Pool, project_id : i64 ) -> sqlx:: Result< Vec< Card> > {
5d90e0e0 Andy McCall 19 days ago
173
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 ↶
174
}
175
176
pub async fn add_column ( pool : & Pool, project_id : i64 , name : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
177
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 ↶
178
.bind ( project_id)
179
.fetch_one ( pool)
180
.await? ;
5d90e0e0 Andy McCall 19 days ago
181
sqlx:: query( " INSERT INTO project_columns (project_id, name, position) VALUES ($1, $2, $3)" )
e1ec9fbf Andy McCall 1 month ago ↶
182
.bind ( project_id)
183
.bind ( name)
184
.bind ( pos)
185
.execute ( pool)
186
.await? ;
187
touch ( pool, project_id) .await
188
}
189
190
pub async fn rename_column ( pool : & Pool, project_id : i64 , column_id : i64 , name : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
191
sqlx:: query( " UPDATE project_columns SET name = $1 WHERE id = $2 AND project_id = $3" )
e1ec9fbf Andy McCall 1 month ago ↶
192
.bind ( name)
193
.bind ( column_id)
194
.bind ( project_id)
195
.execute ( pool)
196
.await? ;
197
touch ( pool, project_id) .await
198
}
199
200
201
pub async fn shift_column ( pool : & Pool, project_id : i64 , column_id : i64 , by : i64 ) -> sqlx:: Result< ( ) > {
202
let cols = columns ( pool, project_id) .await? ;
203
let Some ( i) = cols.iter ( ) .position ( | c | c.id = = column_id ) else { return Ok ( ( ) ) } ;
204
let j = i as i64 + by;
205
if j < 0 | | j as usize > = cols.len ( ) {
206
return Ok ( ( ) ) ;
207
}
208
let mut ids: Vec< i64 > = cols.iter ( ) .map ( | c | c.id ) .collect ( ) ;
209
ids.swap ( i, j as usize ) ;
210
for ( pos, id) in ids.iter ( ) .enumerate ( ) {
5d90e0e0 Andy McCall 19 days ago
211
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 ↶
212
}
213
touch ( pool, project_id) .await
214
}
215
306aec18 Andy McCall 1 day ago ↶
216
217
pub async fn place_column ( pool : & Pool, project_id : i64 , column_id : i64 , position : i64 ) -> sqlx:: Result< ( ) > {
218
let cols = columns ( pool, project_id) .await? ;
219
let Some ( i) = cols.iter ( ) .position ( | c | c.id = = column_id ) else { return Ok ( ( ) ) } ;
220
let mut ids: Vec< i64 > = cols.iter ( ) .map ( | c | c.id ) .collect ( ) ;
221
let id = ids.remove ( i) ;
222
ids.insert ( ( position.max ( 0 ) as usize ) .min ( ids.len ( ) ) , id) ;
223
for ( pos, id) in ids.iter ( ) .enumerate ( ) {
224
sqlx:: query( " UPDATE project_columns SET position = $1 WHERE id = $2" ) .bind ( pos as i64 ) .bind ( id) .execute ( pool) .await? ;
225
}
226
touch ( pool, project_id) .await
227
}
228
e1ec9fbf Andy McCall 1 month ago ↶
229
230
pub async fn remove_column ( pool : & Pool, project_id : i64 , column_id : i64 ) -> sqlx:: Result< bool > {
231
let cols = columns ( pool, project_id) .await? ;
232
let Some ( to) = cols.iter ( ) .find ( | c | c.id ! = column_id ) else { return Ok ( false ) } ;
233
if ! cols.iter ( ) .any ( | c | c.id = = column_id ) {
234
return Ok ( false ) ;
235
}
941abdd6 Andy McCall 1 month ago ↶
236
let mut tx = super :: begin_write( pool) .await? ;
5d90e0e0 Andy McCall 19 days ago
237
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 ↶
238
.bind ( to.id)
239
.fetch_one ( & mut * tx)
240
.await? ;
5d90e0e0 Andy McCall 19 days ago
241
sqlx:: query( " UPDATE project_cards SET column_id = $1, position = position + $2 WHERE column_id = $3" )
e1ec9fbf Andy McCall 1 month ago ↶
242
.bind ( to.id)
243
.bind ( base)
244
.bind ( column_id)
245
.execute ( & mut * tx)
246
.await? ;
5d90e0e0 Andy McCall 19 days ago
247
sqlx:: query( " DELETE FROM project_columns WHERE id = $1" ) .bind ( column_id) .execute ( & mut * tx) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
248
tx.commit ( ) .await? ;
249
touch ( pool, project_id) .await? ;
250
Ok ( true )
251
}
252
253
pub struct NewCard < 'a > {
254
pub column_id : i64 ,
255
pub kind : & 'a str ,
256
pub repo_id : Option< i64 > ,
257
pub number : Option< i64 > ,
258
pub note : & 'a str ,
259
pub added_by : & 'a str ,
260
}
261
262
263
264
pub async fn add_card ( pool : & Pool, project_id : i64 , c : NewCard< '_ > ) -> sqlx:: Result< bool > {
5d90e0e0 Andy McCall 19 days ago
265
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 ↶
266
.bind ( c.column_id)
267
.fetch_one ( pool)
268
.await? ;
269
let r = sqlx:: query(
439ecd87 Andy McCall 11 days ago ↶
270
" 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" ,
e1ec9fbf Andy McCall 1 month ago ↶
271
)
272
.bind ( project_id)
273
.bind ( c.column_id)
274
.bind ( pos)
275
.bind ( c.kind)
276
.bind ( c.repo_id)
277
.bind ( c.number)
278
.bind ( c.note)
279
.bind ( c.added_by)
280
.bind ( now_ms ( ) )
281
.execute ( pool)
282
.await? ;
283
touch ( pool, project_id) .await? ;
284
Ok ( r.rows_affected ( ) = = 1 )
285
}
286
287
288
pub async fn move_card ( pool : & Pool, project_id : i64 , card_id : i64 , column_id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
289
let ok: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM project_columns WHERE id = $1 AND project_id = $2" )
e1ec9fbf Andy McCall 1 month ago ↶
290
.bind ( column_id)
291
.bind ( project_id)
292
.fetch_one ( pool)
293
.await? ;
294
if ok = = 0 {
295
return Ok ( ( ) ) ;
296
}
5d90e0e0 Andy McCall 19 days ago
297
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 ↶
298
.bind ( column_id)
299
.fetch_one ( pool)
300
.await? ;
439ecd87 Andy McCall 11 days ago ↶
301
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 ↶
302
.bind ( column_id)
303
.bind ( pos)
439ecd87 Andy McCall 11 days ago ↶
304
.bind ( now_ms ( ) )
e1ec9fbf Andy McCall 1 month ago ↶
305
.bind ( card_id)
306
.bind ( project_id)
307
.execute ( pool)
308
.await? ;
309
touch ( pool, project_id) .await
310
}
311
456f4197 Andy McCall 11 days ago ↶
312
313
314
pub async fn move_cards ( pool : & Pool, project_id : i64 , card_ids : & [i64 ], column_id : i64 ) -> sqlx:: Result< ( ) > {
315
let mut tx = pool.begin ( ) .await? ;
316
let pos: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = $1" )
317
.bind ( column_id)
318
.fetch_one ( & mut * tx)
319
.await? ;
439ecd87 Andy McCall 11 days ago ↶
320
let now = now_ms ( ) ;
456f4197 Andy McCall 11 days ago ↶
321
for ( i, id) in card_ids.iter ( ) .enumerate ( ) {
439ecd87 Andy McCall 11 days ago ↶
322
sqlx:: query( " UPDATE project_cards SET column_id = $1, position = $2, moved_ms = $3 WHERE id = $4 AND project_id = $5" )
456f4197 Andy McCall 11 days ago ↶
323
.bind ( column_id)
324
.bind ( pos + i as i64 )
439ecd87 Andy McCall 11 days ago ↶
325
.bind ( now)
456f4197 Andy McCall 11 days ago ↶
326
.bind ( id)
327
.bind ( project_id)
328
.execute ( & mut * tx)
329
.await? ;
330
}
439ecd87 Andy McCall 11 days ago ↶
331
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now) .bind ( project_id) .execute ( & mut * tx) .await? ;
456f4197 Andy McCall 11 days ago ↶
332
tx.commit ( ) .await
439ecd87 Andy McCall 11 days ago ↶
333
}
334
335
336
pub async fn archive_cards ( pool : & Pool, project_id : i64 , card_ids : & [i64 ]) -> sqlx:: Result< u64 > {
337
let mut tx = pool.begin ( ) .await? ;
338
let now = now_ms ( ) ;
339
let mut n = 0 ;
340
for id in card_ids {
341
n + = sqlx:: query( " UPDATE project_cards SET archived_ms = $1 WHERE id = $2 AND project_id = $3 AND archived_ms IS NULL" )
342
.bind ( now)
343
.bind ( id)
344
.bind ( project_id)
345
.execute ( & mut * tx)
346
.await?
347
.rows_affected ( ) ;
348
}
349
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now) .bind ( project_id) .execute ( & mut * tx) .await? ;
350
tx.commit ( ) .await? ;
351
Ok ( n)
352
}
353
354
355
356
357
pub async fn restore_card ( pool : & Pool, project_id : i64 , card_id : i64 ) -> sqlx:: Result< bool > {
358
let mut tx = pool.begin ( ) .await? ;
359
let pos: i64 = sqlx:: query_scalar(
360
" SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = (SELECT column_id FROM project_cards WHERE id = $1)" ,
361
)
362
.bind ( card_id)
363
.fetch_one ( & mut * tx)
364
.await? ;
365
let now = now_ms ( ) ;
046e3352 Andy McCall 8 days ago ↶
366
439ecd87 Andy McCall 11 days ago ↶
367
let r = sqlx:: query(
046e3352 Andy McCall 8 days ago ↶
368
" UPDATE project_cards SET archived_ms = NULL, iteration_id = NULL, moved_ms = $1, position = $2 \
369
WHERE id = $3 AND project_id = $4 AND archived_ms IS NOT NULL" ,
439ecd87 Andy McCall 11 days ago ↶
370
)
371
.bind ( now)
372
.bind ( pos)
373
.bind ( card_id)
374
.bind ( project_id)
375
.execute ( & mut * tx)
376
.await? ;
377
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now) .bind ( project_id) .execute ( & mut * tx) .await? ;
378
tx.commit ( ) .await? ;
379
Ok ( r.rows_affected ( ) = = 1 )
380
}
381
382
383
pub fn older_than ( card : & Card, days : i64 , now : i64 ) -> bool {
384
card.moved_ms < now - days * DAY_MS
046e3352 Andy McCall 8 days ago ↶
385
}
386
387
388
pub async fn iterations ( pool : & Pool, project_id : i64 ) -> sqlx:: Result< Vec< Iteration> > {
389
sqlx:: query_as( " SELECT * FROM project_iterations WHERE project_id = $1 ORDER BY start_ms DESC, id DESC" )
390
.bind ( project_id)
391
.fetch_all ( pool)
392
.await
393
}
394
395
396
pub async fn start_iteration ( pool : & Pool, project_id : i64 , name : & str , start_ms : i64 , end_ms : i64 ) -> sqlx:: Result< Option< Iteration> > {
397
let mut tx = super :: begin_write( pool) .await? ;
398
let open: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM project_iterations WHERE project_id = $1 AND closed_ms IS NULL" )
399
.bind ( project_id)
400
.fetch_one ( & mut * tx)
401
.await? ;
402
if open > 0 {
403
return Ok ( None ) ;
404
}
405
let it: Iteration =
406
sqlx:: query_as( " INSERT INTO project_iterations (project_id, name, start_ms, end_ms) VALUES ($1, $2, $3, $4) RETURNING *" )
407
.bind ( project_id)
408
.bind ( name)
409
.bind ( start_ms)
410
.bind ( end_ms)
411
.fetch_one ( & mut * tx)
412
.await? ;
413
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now_ms ( ) ) .bind ( project_id) .execute ( & mut * tx) .await? ;
414
tx.commit ( ) .await? ;
415
Ok ( Some ( it) )
416
}
417
418
419
420
pub fn done_column ( columns : & [Column]) -> Option< i64 > {
421
columns.iter ( ) .find ( | c | c.takes_finished ) .or ( columns.last ( ) ) .map ( | c | c.id )
422
}
423
424
425
426
pub async fn close_iteration ( pool : & Pool, project_id : i64 , iteration_id : i64 , done : Option< i64 > ) -> sqlx:: Result< Option< u64 > > {
427
let mut tx = super :: begin_write( pool) .await? ;
428
let now = now_ms ( ) ;
429
let closed = sqlx:: query( " UPDATE project_iterations SET closed_ms = $1 WHERE id = $2 AND project_id = $3 AND closed_ms IS NULL" )
430
.bind ( now)
431
.bind ( iteration_id)
432
.bind ( project_id)
433
.execute ( & mut * tx)
434
.await? ;
435
if closed.rows_affected ( ) = = 0 {
436
return Ok ( None ) ;
437
}
438
let mut n = 0 ;
439
if let Some ( done) = done {
440
n = sqlx:: query(
441
" UPDATE project_cards SET archived_ms = $1, iteration_id = $2 WHERE project_id = $3 AND column_id = $4 AND archived_ms IS NULL" ,
442
)
443
.bind ( now)
444
.bind ( iteration_id)
445
.bind ( project_id)
446
.bind ( done)
447
.execute ( & mut * tx)
448
.await?
449
.rows_affected ( ) ;
450
}
451
sqlx:: query( " UPDATE projects SET updated_ms = $1 WHERE id = $2" ) .bind ( now) .bind ( project_id) .execute ( & mut * tx) .await? ;
452
tx.commit ( ) .await? ;
453
Ok ( Some ( n) )
454
}
455
456
457
458
459
pub async fn done_times ( pool : & Pool, it : & Iteration, done : Option< i64 > ) -> sqlx:: Result< Vec< i64 > > {
460
sqlx:: query_scalar(
461
" SELECT moved_ms FROM project_cards WHERE project_id = $1 AND (iteration_id = $2 \
462
OR ($3 AND iteration_id IS NULL AND column_id = $4 AND (archived_ms IS NULL OR archived_ms >= $5))) ORDER BY moved_ms" ,
463
)
464
.bind ( it.project_id)
465
.bind ( it.id)
466
.bind ( it.closed_ms.is_none ( ) )
467
.bind ( done.unwrap_or ( - 1 ) )
468
.bind ( it.start_ms)
469
.fetch_all ( pool)
470
.await
471
}
472
473
474
475
476
pub fn burn_up ( times : & [i64 ], start_ms : i64 , end_ms : i64 , now : i64 ) -> Vec< usize > {
477
let days = ( ( end_ms - start_ms + DAY_MS - 1 ) / DAY_MS ) .max ( 1 ) ;
478
let upto = ( ( now.min ( end_ms - 1 ) - start_ms) / DAY_MS + 1 ) .clamp ( 0 , days) ;
479
( 0 .. upto) .map ( | d | times.iter ( ) .filter ( | & & t | t < start_ms + ( d + 1 ) * DAY_MS ) .count ( ) ) .collect ( )
480
}
481
482
483
pub fn iteration_days ( it : & Iteration) -> i64 {
484
( ( it.end_ms - it.start_ms + DAY_MS - 1 ) / DAY_MS ) .max ( 1 )
439ecd87 Andy McCall 11 days ago ↶
485
}
486
946fe7a0 Andy McCall 10 days ago ↶
487
488
489
439ecd87 Andy McCall 11 days ago ↶
490
pub async fn set_archive_after ( pool : & Pool, project_id : i64 , column_id : i64 , days : Option< i64 > ) -> sqlx:: Result< ( ) > {
491
sqlx:: query( " UPDATE project_columns SET archive_after_days = $1 WHERE id = $2 AND project_id = $3" )
492
.bind ( days)
493
.bind ( column_id)
494
.bind ( project_id)
495
.execute ( pool)
496
.await? ;
497
touch ( pool, project_id) .await
498
}
499
946fe7a0 Andy McCall 10 days ago ↶
500
501
502
pub const FINISHED_KEPT_DAYS : i64 = 14 ;
503
504
505
pub const NEVER : i64 = - 1 ;
506
507
508
509
510
511
439ecd87 Andy McCall 11 days ago ↶
512
pub async fn archive_due ( pool : & Pool, now : i64 ) -> sqlx:: Result< u64 > {
946fe7a0 Andy McCall 10 days ago ↶
513
let set = sqlx:: query(
439ecd87 Andy McCall 11 days ago ↶
514
" UPDATE project_cards SET archived_ms = $1 WHERE archived_ms IS NULL AND EXISTS \
946fe7a0 Andy McCall 10 days ago ↶
515
(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 ↶
516
AND project_cards.moved_ms < $1 - c.archive_after_days * $2)" ,
517
)
518
.bind ( now)
519
.bind ( DAY_MS )
946fe7a0 Andy McCall 10 days ago ↶
520
.execute ( pool)
521
.await? ;
522
let finished = sqlx:: query(
523
" UPDATE project_cards SET archived_ms = $1 WHERE archived_ms IS NULL AND moved_ms < $2 \
524
AND EXISTS (SELECT 1 FROM project_columns c WHERE c.id = project_cards.column_id AND c.archive_after_days IS NULL) \
525
AND ((kind = 'issue' AND EXISTS (SELECT 1 FROM issues i WHERE i.repo_id = project_cards.repo_id \
526
AND i.number = project_cards.number AND i.state = 'closed')) \
527
OR (kind = 'proposal' AND EXISTS (SELECT 1 FROM proposals p WHERE p.repo_id = project_cards.repo_id \
528
AND p.number = project_cards.number AND p.state IN ('closed', 'landed'))))" ,
529
)
530
.bind ( now)
531
.bind ( now - FINISHED_KEPT_DAYS * DAY_MS )
439ecd87 Andy McCall 11 days ago ↶
532
.execute ( pool)
533
.await? ;
946fe7a0 Andy McCall 10 days ago ↶
534
Ok ( set.rows_affected ( ) + finished.rows_affected ( ) )
456f4197 Andy McCall 11 days ago ↶
535
}
536
e1ec9fbf Andy McCall 1 month ago ↶
537
538
pub async fn shift_card ( pool : & Pool, project_id : i64 , card_id : i64 , by : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
539
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 ↶
540
.bind ( card_id)
541
.bind ( project_id)
542
.fetch_optional ( pool)
543
.await?
544
else {
545
return Ok ( ( ) ) ;
546
} ;
5d90e0e0 Andy McCall 19 days ago
547
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 ↶
548
.bind ( card.column_id)
549
.fetch_all ( pool)
550
.await? ;
551
let Some ( i) = col.iter ( ) .position ( | c | c.id = = card_id ) else { return Ok ( ( ) ) } ;
552
let j = i as i64 + by;
553
if j < 0 | | j as usize > = col.len ( ) {
554
return Ok ( ( ) ) ;
555
}
556
let mut ids: Vec< i64 > = col.iter ( ) .map ( | c | c.id ) .collect ( ) ;
557
ids.swap ( i, j as usize ) ;
558
for ( pos, id) in ids.iter ( ) .enumerate ( ) {
5d90e0e0 Andy McCall 19 days ago
559
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 ↶
560
}
561
touch ( pool, project_id) .await
562
}
563
564
pub async fn remove_card ( pool : & Pool, project_id : i64 , card_id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
565
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 ↶
566
touch ( pool, project_id) .await
567
}
568
569
1000b40a Andy McCall 10 days ago ↶
570
571
pub async fn set_takes_finished ( pool : & Pool, project_id : i64 , column_id : Option< i64 > ) -> sqlx:: Result< ( ) > {
572
let mut tx = super :: begin_write( pool) .await? ;
573
sqlx:: query( " UPDATE project_columns SET takes_finished = FALSE WHERE project_id = $1" ) .bind ( project_id) .execute ( & mut * tx) .await? ;
574
if let Some ( c) = column_id {
575
sqlx:: query( " UPDATE project_columns SET takes_finished = TRUE WHERE id = $1 AND project_id = $2" )
576
.bind ( c)
577
.bind ( project_id)
578
.execute ( & mut * tx)
579
.await? ;
580
}
581
tx.commit ( ) .await? ;
582
touch ( pool, project_id) .await
583
}
584
585
586
587
588
pub async fn finished ( pool : & Pool, repo_id : i64 , number : i64 ) -> sqlx:: Result< ( ) > {
589
let cards: Vec< ( i64 , i64 , i64 ) > = sqlx:: query_as(
590
" SELECT c.id, c.project_id, c.column_id FROM project_cards c
591
WHERE c.repo_id = $1 AND c.number = $2 AND c.kind <> 'note' AND c.archived_ms IS NULL" ,
592
)
593
.bind ( repo_id)
594
.bind ( number)
595
.fetch_all ( pool)
596
.await? ;
597
for ( card, project, column) in cards {
598
let to: Option< i64 > =
599
sqlx:: query_scalar( " SELECT id FROM project_columns WHERE project_id = $1 AND takes_finished ORDER BY position LIMIT 1" )
600
.bind ( project)
601
.fetch_optional ( pool)
602
.await? ;
603
if let Some ( to) = to.filter ( | to | * to ! = column) {
604
move_card ( pool, project, card, to) .await? ;
605
}
606
}
607
Ok ( ( ) )
608
}
609
e1ec9fbf Andy McCall 1 month ago ↶
610
pub async fn placements ( pool : & Pool, repo_id : i64 , number : i64 ) -> sqlx:: Result< Vec< ( Project, String) > > {
439ecd87 Andy McCall 11 days ago ↶
611
let rows: Vec< ( i64 , String, Option< i64 > ) > = sqlx:: query_as(
612
" 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 ↶
613
)
614
.bind ( repo_id)
615
.bind ( number)
616
.fetch_all ( pool)
617
.await? ;
618
let mut out = Vec :: new( ) ;
439ecd87 Andy McCall 11 days ago ↶
619
for ( pid, col, archived) in rows {
620
let col = if archived.is_some ( ) { format! ( " {col} , archived" ) } else { col } ;
5d90e0e0 Andy McCall 19 days ago
621
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 ↶
622
out.push ( ( p, col) ) ;
623
}
624
}
625
Ok ( out)
626
}