@@ -29,21 +29,69 @@ pub struct SqliteStore {
29
29
}
30
30
31
31
impl SqliteStore {
32
+ /// constructs a new SqliteStore from an existing
33
+ /// sqlx::SqlitePool. the default table name for this session
34
+ /// store will be "async_sessions". To override this, chain this
35
+ /// with [`with_table_name`](crate::SqliteStore::with_table_name).
36
+ ///
37
+ /// ```rust
38
+ /// # use async_sqlx_session::SqliteStore;
39
+ /// # use async_session::Result;
40
+ /// # fn main() -> Result { async_std::task::block_on(async {
41
+ /// let pool = sqlx::SqlitePool::new("sqlite:%3Amemory:").await.unwrap();
42
+ /// let store = SqliteStore::from_client(pool)
43
+ /// .with_table_name("custom_table_name");
44
+ /// store.migrate().await;
45
+ /// # Ok(()) }) }
46
+ /// ```
32
47
pub fn from_client ( client : SqlitePool ) -> Self {
33
48
Self {
34
49
client,
35
50
table_name : "async_sessions" . into ( ) ,
36
51
}
37
52
}
38
53
54
+ /// constructs a new SqliteStore from a sqlite: database url. the
55
+ /// default table name for this session store will be
56
+ /// "async_sessions". To override this, either chain with
57
+ /// [`with_table_name`](crate::SqliteStore::with_table_name) or
58
+ /// use
59
+ /// [`new_with_table_name`](crate::SqliteStore::new_with_table_name)
60
+ ///
61
+ /// ```rust
62
+ /// # use async_sqlx_session::SqliteStore;
63
+ /// # use async_session::Result;
64
+ /// # fn main() -> Result { async_std::task::block_on(async {
65
+ /// let store = SqliteStore::new("sqlite:%3Amemory:").await?
66
+ /// .with_table_name("custom_table_name");
67
+ /// store.migrate().await;
68
+ /// # Ok(()) }) }
69
+ /// ```
39
70
pub async fn new ( database_url : & str ) -> sqlx:: Result < Self > {
40
71
Ok ( Self :: from_client ( SqlitePool :: new ( database_url) . await ?) )
41
72
}
42
73
74
+ /// constructs a new SqliteStore from a sqlite: database url. the
75
+ /// default table name for this session store will be
76
+ /// "async_sessions". To override this, either chain with
77
+ /// [`with_table_name`](crate::SqliteStore::with_table_name) or
78
+ /// use
79
+ /// [`new_with_table_name`](crate::SqliteStore::new_with_table_name)
80
+ ///
81
+ /// ```rust
82
+ /// # use async_sqlx_session::SqliteStore;
83
+ /// # use async_session::Result;
84
+ /// # fn main() -> Result { async_std::task::block_on(async {
85
+ /// let store = SqliteStore::new_with_table_name("sqlite:%3Amemory:", "custom_table_name").await?;
86
+ /// store.migrate().await;
87
+ /// # Ok(()) }) }
88
+ /// ```
43
89
pub async fn new_with_table_name ( database_url : & str , table_name : & str ) -> sqlx:: Result < Self > {
44
90
Ok ( Self :: new ( database_url) . await ?. with_table_name ( table_name) )
45
91
}
46
92
93
+ /// Chainable method to add a custom table name. This will panic
94
+ /// if the table name is not `[a-zA-Z0-9_-]+`.
47
95
pub fn with_table_name ( mut self , table_name : impl AsRef < str > ) -> Self {
48
96
let table_name = table_name. as_ref ( ) ;
49
97
if table_name. is_empty ( )
@@ -61,6 +109,11 @@ impl SqliteStore {
61
109
self
62
110
}
63
111
112
+ /// Creates a session table if it does not already exist. If it
113
+ /// does, this will noop, making it safe to call repeatedly on
114
+ /// store initialization. In the future, this may make
115
+ /// exactly-once modifications to the schema of the session table
116
+ /// on breaking releases.
64
117
pub async fn migrate ( & self ) -> sqlx:: Result < ( ) > {
65
118
log:: info!( "migrating sessions on `{}`" , self . table_name) ;
66
119
@@ -87,6 +140,8 @@ impl SqliteStore {
87
140
self . client . acquire ( ) . await
88
141
}
89
142
143
+ /// Spawns an async_std::task that clears out stale (expired)
144
+ /// sessions on a periodic basis.
90
145
pub fn spawn_cleanup_task ( & self , period : Duration ) -> task:: JoinHandle < ( ) > {
91
146
let store = self . clone ( ) ;
92
147
task:: spawn ( async move {
@@ -99,6 +154,8 @@ impl SqliteStore {
99
154
} )
100
155
}
101
156
157
+ /// Performs a one-time cleanup task that clears out stale
158
+ /// (expired) sessions. You may want to call this from cron.
102
159
pub async fn cleanup ( & self ) -> sqlx:: Result < ( ) > {
103
160
let mut connection = self . connection ( ) . await ?;
104
161
sqlx:: query ( & self . substitute_table_name (
@@ -113,6 +170,14 @@ impl SqliteStore {
113
170
114
171
Ok ( ( ) )
115
172
}
173
+
174
+ pub async fn count ( & self ) -> sqlx:: Result < i32 > {
175
+ let ( count, ) = sqlx:: query_as ( "select count(*) from async_sessions" )
176
+ . fetch_one ( & mut self . connection ( ) . await ?)
177
+ . await ?;
178
+
179
+ Ok ( count)
180
+ }
116
181
}
117
182
118
183
#[ async_trait]
@@ -124,7 +189,7 @@ impl SessionStore for SqliteStore {
124
189
let ( session, ) : ( String , ) = sqlx:: query_as ( & self . substitute_table_name (
125
190
r#"
126
191
SELECT session FROM %%TABLE_NAME%%
127
- WHERE id = ? AND (expires IS NULL || expires > ?)
192
+ WHERE id = ? AND (expires IS NULL OR expires > ?)
128
193
"# ,
129
194
) )
130
195
. bind ( & id)
@@ -141,7 +206,7 @@ impl SessionStore for SqliteStore {
141
206
let string = serde_json:: to_string ( & session) . ok ( ) ?;
142
207
let mut connection = self . connection ( ) . await . ok ( ) ?;
143
208
144
- let result = sqlx:: query ( & self . substitute_table_name (
209
+ sqlx:: query ( & self . substitute_table_name (
145
210
r#"
146
211
INSERT INTO %%TABLE_NAME%%
147
212
(id, session, expires) VALUES (?, ?, ?)
@@ -154,14 +219,10 @@ impl SessionStore for SqliteStore {
154
219
. bind ( & string)
155
220
. bind ( & session. expiry ( ) . map ( |expiry| expiry. timestamp ( ) ) )
156
221
. execute ( & mut connection)
157
- . await ;
222
+ . await
223
+ . ok ( ) ?;
158
224
159
- if let Err ( e) = result {
160
- dbg ! ( e) ;
161
- None
162
- } else {
163
- session. into_cookie_value ( )
164
- }
225
+ session. into_cookie_value ( )
165
226
}
166
227
167
228
async fn destroy_session ( & self , session : Session ) -> Result {
@@ -192,3 +253,174 @@ impl SessionStore for SqliteStore {
192
253
Ok ( ( ) )
193
254
}
194
255
}
256
+
257
+ #[ cfg( test) ]
258
+ mod tests {
259
+ use super :: * ;
260
+
261
+ async fn test_store ( ) -> SqliteStore {
262
+ let store = SqliteStore :: new ( "sqlite:%3Amemory:" )
263
+ . await
264
+ . expect ( "building a sqlite :memory: SqliteStore" ) ;
265
+ store
266
+ . migrate ( )
267
+ . await
268
+ . expect ( "migrating a brand new :memory: SqliteStore" ) ;
269
+ store
270
+ }
271
+
272
+ #[ async_std:: test]
273
+ async fn creating_a_new_session_with_no_expiry ( ) -> Result {
274
+ let store = test_store ( ) . await ;
275
+ let session = Session :: new ( ) ;
276
+ session. insert ( "key" . into ( ) , "value" . into ( ) ) ;
277
+ let cloned = session. clone ( ) ;
278
+ let cookie_value = store. store_session ( session) . await . unwrap ( ) ;
279
+
280
+ let ( id, expires, serialized, count) : ( String , Option < i64 > , String , i64 ) =
281
+ sqlx:: query_as ( "select id, expires, session, count(*) from async_sessions" )
282
+ . fetch_one ( & mut store. connection ( ) . await ?)
283
+ . await ?;
284
+
285
+ assert_eq ! ( 1 , count) ;
286
+ assert_eq ! ( id, cloned. id( ) ) ;
287
+ assert_eq ! ( expires, None ) ;
288
+
289
+ let deserialized_session: Session = serde_json:: from_str ( & serialized) ?;
290
+ assert_eq ! ( cloned. id( ) , deserialized_session. id( ) ) ;
291
+ assert_eq ! ( "value" , deserialized_session. get( "key" ) . unwrap( ) ) ;
292
+
293
+ let loaded_session = store. load_session ( cookie_value) . await . unwrap ( ) ;
294
+ assert_eq ! ( cloned. id( ) , loaded_session. id( ) ) ;
295
+ assert_eq ! ( "value" , loaded_session. get( "key" ) . unwrap( ) ) ;
296
+
297
+ assert ! ( !loaded_session. is_expired( ) ) ;
298
+ Ok ( ( ) )
299
+ }
300
+
301
+ #[ async_std:: test]
302
+ async fn updating_a_session ( ) -> Result {
303
+ let store = test_store ( ) . await ;
304
+ let session = Session :: new ( ) ;
305
+ let original_id = session. id ( ) . to_owned ( ) ;
306
+
307
+ session. insert ( "key" . into ( ) , "value" . into ( ) ) ;
308
+ let cookie_value = store. store_session ( session) . await . unwrap ( ) ;
309
+
310
+ let session = store. load_session ( cookie_value. clone ( ) ) . await . unwrap ( ) ;
311
+ session. insert ( "key" . into ( ) , "other value" . into ( ) ) ;
312
+ assert_eq ! ( None , store. store_session( session) . await ) ;
313
+
314
+ let session = store. load_session ( cookie_value. clone ( ) ) . await . unwrap ( ) ;
315
+ assert_eq ! ( session. get( "key" ) . unwrap( ) , "other value" ) ;
316
+
317
+ let ( id, count) : ( String , i64 ) = sqlx:: query_as ( "select id, count(*) from async_sessions" )
318
+ . fetch_one ( & mut store. connection ( ) . await ?)
319
+ . await ?;
320
+
321
+ assert_eq ! ( 1 , count) ;
322
+ assert_eq ! ( original_id, id) ;
323
+
324
+ Ok ( ( ) )
325
+ }
326
+
327
+ #[ async_std:: test]
328
+ async fn updating_a_session_extending_expiry ( ) -> Result {
329
+ let store = test_store ( ) . await ;
330
+ let mut session = Session :: new ( ) ;
331
+ session. expire_in ( Duration :: from_secs ( 10 ) ) ;
332
+ let original_id = session. id ( ) . to_owned ( ) ;
333
+ let original_expires = session. expiry ( ) . unwrap ( ) . clone ( ) ;
334
+ let cookie_value = store. store_session ( session) . await . unwrap ( ) ;
335
+
336
+ let mut session = store. load_session ( cookie_value. clone ( ) ) . await . unwrap ( ) ;
337
+ assert_eq ! ( session. expiry( ) . unwrap( ) , & original_expires) ;
338
+ session. expire_in ( Duration :: from_secs ( 20 ) ) ;
339
+ let new_expires = session. expiry ( ) . unwrap ( ) . clone ( ) ;
340
+ store. store_session ( session) . await ;
341
+
342
+ let session = store. load_session ( cookie_value. clone ( ) ) . await . unwrap ( ) ;
343
+ assert_eq ! ( session. expiry( ) . unwrap( ) , & new_expires) ;
344
+
345
+ let ( id, expires, count) : ( String , i64 , i64 ) =
346
+ sqlx:: query_as ( "select id, expires, count(*) from async_sessions" )
347
+ . fetch_one ( & mut store. connection ( ) . await ?)
348
+ . await ?;
349
+
350
+ assert_eq ! ( 1 , count) ;
351
+ assert_eq ! ( expires, new_expires. timestamp( ) ) ;
352
+ assert_eq ! ( original_id, id) ;
353
+
354
+ Ok ( ( ) )
355
+ }
356
+
357
+ #[ async_std:: test]
358
+ async fn creating_a_new_session_with_expiry ( ) -> Result {
359
+ let store = test_store ( ) . await ;
360
+ let mut session = Session :: new ( ) ;
361
+ session. expire_in ( Duration :: from_secs ( 1 ) ) ;
362
+ session. insert ( "key" . into ( ) , "value" . into ( ) ) ;
363
+ let cloned = session. clone ( ) ;
364
+
365
+ let cookie_value = store. store_session ( session) . await . unwrap ( ) ;
366
+
367
+ let ( id, expires, serialized, count) : ( String , Option < i64 > , String , i64 ) =
368
+ sqlx:: query_as ( "select id, expires, session, count(*) from async_sessions" )
369
+ . fetch_one ( & mut store. connection ( ) . await ?)
370
+ . await ?;
371
+
372
+ assert_eq ! ( 1 , count) ;
373
+ assert_eq ! ( id, cloned. id( ) ) ;
374
+ assert ! ( expires. unwrap( ) > Utc :: now( ) . timestamp( ) ) ;
375
+ dbg ! ( expires. unwrap( ) - Utc :: now( ) . timestamp( ) ) ;
376
+
377
+ let deserialized_session: Session = serde_json:: from_str ( & serialized) ?;
378
+ assert_eq ! ( cloned. id( ) , deserialized_session. id( ) ) ;
379
+ assert_eq ! ( "value" , deserialized_session. get( "key" ) . unwrap( ) ) ;
380
+
381
+ let loaded_session = store. load_session ( cookie_value. clone ( ) ) . await . unwrap ( ) ;
382
+ assert_eq ! ( cloned. id( ) , loaded_session. id( ) ) ;
383
+ assert_eq ! ( "value" , loaded_session. get( "key" ) . unwrap( ) ) ;
384
+
385
+ assert ! ( !loaded_session. is_expired( ) ) ;
386
+
387
+ task:: sleep ( Duration :: from_secs ( 1 ) ) . await ;
388
+ assert_eq ! ( None , store. load_session( cookie_value) . await ) ;
389
+
390
+ Ok ( ( ) )
391
+ }
392
+
393
+ #[ async_std:: test]
394
+ async fn destroying_a_single_session ( ) -> Result {
395
+ let store = test_store ( ) . await ;
396
+ for _ in 0 ..3i8 {
397
+ store. store_session ( Session :: new ( ) ) . await ;
398
+ }
399
+
400
+ let cookie = store. store_session ( Session :: new ( ) ) . await . unwrap ( ) ;
401
+ dbg ! ( "storing" ) ;
402
+ assert_eq ! ( 4 , store. count( ) . await ?) ;
403
+ let session = store. load_session ( cookie. clone ( ) ) . await . unwrap ( ) ;
404
+ store. destroy_session ( session. clone ( ) ) . await . unwrap ( ) ;
405
+ assert_eq ! ( None , store. load_session( cookie) . await ) ;
406
+ assert_eq ! ( 3 , store. count( ) . await ?) ;
407
+
408
+ // attempting to destroy the session again is not an error
409
+ assert ! ( store. destroy_session( session) . await . is_ok( ) ) ;
410
+ Ok ( ( ) )
411
+ }
412
+
413
+ #[ async_std:: test]
414
+ async fn clearing_the_whole_store ( ) -> Result {
415
+ let store = test_store ( ) . await ;
416
+ for _ in 0 ..3i8 {
417
+ store. store_session ( Session :: new ( ) ) . await ;
418
+ }
419
+
420
+ assert_eq ! ( 3 , store. count( ) . await ?) ;
421
+ store. clear_store ( ) . await . unwrap ( ) ;
422
+ assert_eq ! ( 0 , store. count( ) . await ?) ;
423
+
424
+ Ok ( ( ) )
425
+ }
426
+ }
0 commit comments