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 ,
25
}
26
27
# [ derive ( Clone, Debug, FromRow ) ]
28
pub struct Card {
29
pub id : i64 ,
30
pub project_id : i64 ,
31
pub column_id : i64 ,
32
pub position : i64 ,
33
pub kind : String,
34
pub repo_id : Option< i64 > ,
35
pub number : Option< i64 > ,
36
pub note : String,
37
pub added_by : String,
38
pub created_ms : i64 ,
39
}
40
41
pub const DEFAULT_COLUMNS : [ & str ; 3 ] = [ " Backlog" , " Doing" , " Done" ] ;
42
43
pub async fn create ( pool : & Pool, owner : & str , title : & str , description : & str , public : bool ) -> sqlx:: Result< Project> {
941abdd6 Andy McCall 1 month ago ↶
44
let mut tx = super :: begin_write( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
45
let number: i64 =
46
sqlx:: query_scalar( " SELECT COALESCE(MAX(number), 0) + 1 FROM projects WHERE owner = ?" ) .bind ( owner) .fetch_one ( & mut * tx) .await? ;
47
let now = now_ms ( ) ;
48
let p: Project = sqlx:: query_as(
49
" INSERT INTO projects (owner, number, title, description, public, created_ms, updated_ms) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *" ,
50
)
51
.bind ( owner)
52
.bind ( number)
53
.bind ( title)
54
.bind ( description)
55
.bind ( public)
56
.bind ( now)
57
.bind ( now)
58
.fetch_one ( & mut * tx)
59
.await? ;
60
for ( i, name) in DEFAULT_COLUMNS .iter ( ) .enumerate ( ) {
61
sqlx:: query( " INSERT INTO project_columns (project_id, name, position) VALUES (?, ?, ?)" )
62
.bind ( p.id)
63
.bind ( name)
64
.bind ( i as i64 )
65
.execute ( & mut * tx)
66
.await? ;
67
}
68
tx.commit ( ) .await? ;
69
Ok ( p)
70
}
71
72
pub async fn of_owner ( pool : & Pool, owner : & str ) -> sqlx:: Result< Vec< Project> > {
73
sqlx:: query_as( " SELECT * FROM projects WHERE owner = ? ORDER BY closed, updated_ms DESC" ) .bind ( owner) .fetch_all ( pool) .await
74
}
75
76
pub async fn get ( pool : & Pool, owner : & str , number : i64 ) -> sqlx:: Result< Option< Project> > {
77
sqlx:: query_as( " SELECT * FROM projects WHERE owner = ? AND number = ?" ) .bind ( owner) .bind ( number) .fetch_optional ( pool) .await
78
}
79
80
pub async fn set ( pool : & Pool, id : i64 , title : & str , description : & str , public : bool , closed : bool ) -> sqlx:: Result< ( ) > {
81
sqlx:: query( " UPDATE projects SET title = ?, description = ?, public = ?, closed = ?, updated_ms = ? WHERE id = ?" )
82
.bind ( title)
83
.bind ( description)
84
.bind ( public)
85
.bind ( closed)
86
.bind ( now_ms ( ) )
87
.bind ( id)
88
.execute ( pool)
89
.await? ;
90
Ok ( ( ) )
91
}
92
93
pub async fn delete ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
94
sqlx:: query( " DELETE FROM projects WHERE id = ?" ) .bind ( id) .execute ( pool) .await? ;
95
Ok ( ( ) )
96
}
97
98
async fn touch ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
99
sqlx:: query( " UPDATE projects SET updated_ms = ? WHERE id = ?" ) .bind ( now_ms ( ) ) .bind ( id) .execute ( pool) .await? ;
100
Ok ( ( ) )
101
}
102
103
pub async fn columns ( pool : & Pool, project_id : i64 ) -> sqlx:: Result< Vec< Column> > {
104
sqlx:: query_as( " SELECT * FROM project_columns WHERE project_id = ? ORDER BY position, id" ) .bind ( project_id) .fetch_all ( pool) .await
105
}
106
107
pub async fn cards ( pool : & Pool, project_id : i64 ) -> sqlx:: Result< Vec< Card> > {
108
sqlx:: query_as( " SELECT * FROM project_cards WHERE project_id = ? ORDER BY position, id" ) .bind ( project_id) .fetch_all ( pool) .await
109
}
110
111
pub async fn add_column ( pool : & Pool, project_id : i64 , name : & str ) -> sqlx:: Result< ( ) > {
112
let pos: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_columns WHERE project_id = ?" )
113
.bind ( project_id)
114
.fetch_one ( pool)
115
.await? ;
116
sqlx:: query( " INSERT INTO project_columns (project_id, name, position) VALUES (?, ?, ?)" )
117
.bind ( project_id)
118
.bind ( name)
119
.bind ( pos)
120
.execute ( pool)
121
.await? ;
122
touch ( pool, project_id) .await
123
}
124
125
pub async fn rename_column ( pool : & Pool, project_id : i64 , column_id : i64 , name : & str ) -> sqlx:: Result< ( ) > {
126
sqlx:: query( " UPDATE project_columns SET name = ? WHERE id = ? AND project_id = ?" )
127
.bind ( name)
128
.bind ( column_id)
129
.bind ( project_id)
130
.execute ( pool)
131
.await? ;
132
touch ( pool, project_id) .await
133
}
134
135
136
pub async fn shift_column ( pool : & Pool, project_id : i64 , column_id : i64 , by : i64 ) -> sqlx:: Result< ( ) > {
137
let cols = columns ( pool, project_id) .await? ;
138
let Some ( i) = cols.iter ( ) .position ( | c | c.id = = column_id ) else { return Ok ( ( ) ) } ;
139
let j = i as i64 + by;
140
if j < 0 | | j as usize > = cols.len ( ) {
141
return Ok ( ( ) ) ;
142
}
143
let mut ids: Vec< i64 > = cols.iter ( ) .map ( | c | c.id ) .collect ( ) ;
144
ids.swap ( i, j as usize ) ;
145
for ( pos, id) in ids.iter ( ) .enumerate ( ) {
146
sqlx:: query( " UPDATE project_columns SET position = ? WHERE id = ?" ) .bind ( pos as i64 ) .bind ( id) .execute ( pool) .await? ;
147
}
148
touch ( pool, project_id) .await
149
}
150
151
152
pub async fn remove_column ( pool : & Pool, project_id : i64 , column_id : i64 ) -> sqlx:: Result< bool > {
153
let cols = columns ( pool, project_id) .await? ;
154
let Some ( to) = cols.iter ( ) .find ( | c | c.id ! = column_id ) else { return Ok ( false ) } ;
155
if ! cols.iter ( ) .any ( | c | c.id = = column_id ) {
156
return Ok ( false ) ;
157
}
941abdd6 Andy McCall 1 month ago ↶
158
let mut tx = super :: begin_write( pool) .await? ;
e1ec9fbf Andy McCall 1 month ago ↶
159
let base: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = ?" )
160
.bind ( to.id)
161
.fetch_one ( & mut * tx)
162
.await? ;
163
sqlx:: query( " UPDATE project_cards SET column_id = ?, position = position + ? WHERE column_id = ?" )
164
.bind ( to.id)
165
.bind ( base)
166
.bind ( column_id)
167
.execute ( & mut * tx)
168
.await? ;
169
sqlx:: query( " DELETE FROM project_columns WHERE id = ?" ) .bind ( column_id) .execute ( & mut * tx) .await? ;
170
tx.commit ( ) .await? ;
171
touch ( pool, project_id) .await? ;
172
Ok ( true )
173
}
174
175
pub struct NewCard < 'a > {
176
pub column_id : i64 ,
177
pub kind : & 'a str ,
178
pub repo_id : Option< i64 > ,
179
pub number : Option< i64 > ,
180
pub note : & 'a str ,
181
pub added_by : & 'a str ,
182
}
183
184
185
186
pub async fn add_card ( pool : & Pool, project_id : i64 , c : NewCard< '_ > ) -> sqlx:: Result< bool > {
187
let pos: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = ?" )
188
.bind ( c.column_id)
189
.fetch_one ( pool)
190
.await? ;
191
let r = sqlx:: query(
192
" INSERT OR IGNORE INTO project_cards (project_id, column_id, position, kind, repo_id, number, note, added_by, created_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" ,
193
)
194
.bind ( project_id)
195
.bind ( c.column_id)
196
.bind ( pos)
197
.bind ( c.kind)
198
.bind ( c.repo_id)
199
.bind ( c.number)
200
.bind ( c.note)
201
.bind ( c.added_by)
202
.bind ( now_ms ( ) )
203
.execute ( pool)
204
.await? ;
205
touch ( pool, project_id) .await? ;
206
Ok ( r.rows_affected ( ) = = 1 )
207
}
208
209
210
pub async fn move_card ( pool : & Pool, project_id : i64 , card_id : i64 , column_id : i64 ) -> sqlx:: Result< ( ) > {
211
let ok: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM project_columns WHERE id = ? AND project_id = ?" )
212
.bind ( column_id)
213
.bind ( project_id)
214
.fetch_one ( pool)
215
.await? ;
216
if ok = = 0 {
217
return Ok ( ( ) ) ;
218
}
219
let pos: i64 = sqlx:: query_scalar( " SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = ?" )
220
.bind ( column_id)
221
.fetch_one ( pool)
222
.await? ;
223
sqlx:: query( " UPDATE project_cards SET column_id = ?, position = ? WHERE id = ? AND project_id = ?" )
224
.bind ( column_id)
225
.bind ( pos)
226
.bind ( card_id)
227
.bind ( project_id)
228
.execute ( pool)
229
.await? ;
230
touch ( pool, project_id) .await
231
}
232
233
234
pub async fn shift_card ( pool : & Pool, project_id : i64 , card_id : i64 , by : i64 ) -> sqlx:: Result< ( ) > {
235
let Some ( card) : Option< Card> = sqlx:: query_as( " SELECT * FROM project_cards WHERE id = ? AND project_id = ?" )
236
.bind ( card_id)
237
.bind ( project_id)
238
.fetch_optional ( pool)
239
.await?
240
else {
241
return Ok ( ( ) ) ;
242
} ;
243
let col: Vec< Card> = sqlx:: query_as( " SELECT * FROM project_cards WHERE column_id = ? ORDER BY position, id" )
244
.bind ( card.column_id)
245
.fetch_all ( pool)
246
.await? ;
247
let Some ( i) = col.iter ( ) .position ( | c | c.id = = card_id ) else { return Ok ( ( ) ) } ;
248
let j = i as i64 + by;
249
if j < 0 | | j as usize > = col.len ( ) {
250
return Ok ( ( ) ) ;
251
}
252
let mut ids: Vec< i64 > = col.iter ( ) .map ( | c | c.id ) .collect ( ) ;
253
ids.swap ( i, j as usize ) ;
254
for ( pos, id) in ids.iter ( ) .enumerate ( ) {
255
sqlx:: query( " UPDATE project_cards SET position = ? WHERE id = ?" ) .bind ( pos as i64 ) .bind ( id) .execute ( pool) .await? ;
256
}
257
touch ( pool, project_id) .await
258
}
259
260
pub async fn remove_card ( pool : & Pool, project_id : i64 , card_id : i64 ) -> sqlx:: Result< ( ) > {
261
sqlx:: query( " DELETE FROM project_cards WHERE id = ? AND project_id = ?" ) .bind ( card_id) .bind ( project_id) .execute ( pool) .await? ;
262
touch ( pool, project_id) .await
263
}
264
265
266
pub async fn placements ( pool : & Pool, repo_id : i64 , number : i64 ) -> sqlx:: Result< Vec< ( Project, String) > > {
267
let rows: Vec< ( i64 , String) > = sqlx:: query_as(
268
" SELECT c.project_id, col.name FROM project_cards c JOIN project_columns col ON col.id = c.column_id WHERE c.repo_id = ? AND c.number = ? AND c.kind <> 'note'" ,
269
)
270
.bind ( repo_id)
271
.bind ( number)
272
.fetch_all ( pool)
273
.await? ;
274
let mut out = Vec :: new( ) ;
275
for ( pid, col) in rows {
276
if let Some ( p) = sqlx:: query_as:: < _ , Project> ( " SELECT * FROM projects WHERE id = ?" ) .bind ( pid) .fetch_optional ( pool) .await? {
277
out.push ( ( p, col) ) ;
278
}
279
}
280
Ok ( out)
281
}