summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-10-18 23:42:08 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-10-18 23:42:08 -0400
commit82338ddcb7f14ffbd584a954689f02b6e6a7988e (patch)
tree1e0a525766ca45067bb122cad3af69437db504ca
parentbde5aea211e9838b4511a2b57c6a256fe89b66ab (diff)
parent17b62b3458e3a992b93cd485b05d3fb112dd349a (diff)
Merge branch 'wip/retain-deleted'
-rw-r--r--.sqlx/query-152cbd56cdce2ee7e94ae2568c112d0f6fe2aa136f0758a68f1eb66787db52f0.json50
-rw-r--r--.sqlx/query-15ec7a8aa11908f8b2022accdde2e9e4a49e52e74c3d0e28e82a0b01a3c9887f.json20
-rw-r--r--.sqlx/query-1654b05159c27f74cb333586018161718e2a6788413bffffb252de3e1959f341.json38
-rw-r--r--.sqlx/query-191255b9e55c9b36d0fd047e56e515d121964d3481e48aae3558b53a5123ce7d.json50
-rw-r--r--.sqlx/query-33f9a143409e6f436ed6b64abfbb9e06a5889460743202ca7956acabf006843e.json20
-rw-r--r--.sqlx/query-34d3bc2b18bc0813cbaafc1ef99603f820d3b391f212c2c0883506a53ee757ca.json62
-rw-r--r--.sqlx/query-400de8e1dca27c48362b060c5b16e9ae908c4ea30e2d682d59d4720699b8de91.json62
-rw-r--r--.sqlx/query-487092b18b01005fdb446c11f4726612141f7242decad1de5d9d5f166cb04990.json20
-rw-r--r--.sqlx/query-647c3387fd01747b6318b946d2ea19a3007558abe81a1635c64ef7960acd30b2.json50
-rw-r--r--.sqlx/query-814400dfe985ef284db22c89d6e1a0eb2131b6581759a5028afb6a0910284803.json12
-rw-r--r--.sqlx/query-836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418.json2
-rw-r--r--.sqlx/query-8d9b83fb53e4191ee58b68e65e101d0941f2a266679209f793fd27466b9e3719.json50
-rw-r--r--.sqlx/query-8e816ccef8ecee937d88ff31ca611395405093dbbed738579d715b2d623921be.json62
-rw-r--r--.sqlx/query-8eebba45e8267b9eebc7b1ae7e18ecb51d54d1e304dcbd847de145c0c75423a7.json20
-rw-r--r--.sqlx/query-9386cdaa2cb41f5a7e19d2fc8c187294a4661c18c2d820f4379dfd82138a8f77.json38
-rw-r--r--.sqlx/query-9da5b746de5d481eeb0755283b4094fa9dfa65dc95991f689355889606ab1c46.json (renamed from .sqlx/query-023b1e263b68a483704ae5f6b07e69ab792b7365f2eb2831f7a2ac13e2ecf323.json)8
-rw-r--r--.sqlx/query-9f83f0365b174ce61adb6d48f3b75adcb3ddf8a88439aa8ddb0c66be2f0b384e.json50
-rw-r--r--.sqlx/query-afe4404b44bd8ce6c9a51aafabe3524caa9882d0cba0dc4359eb62cf8154c92d.json50
-rw-r--r--.sqlx/query-b1ba724a62135a6fbcb7ad314ba09dbe4e42fd49b2007e47bcfb07b5ba9001b0.json20
-rw-r--r--.sqlx/query-b7e05e2b2eb5484c954bcedf5de8c7d9ad11bb09a0d0d243181d97c79d771071.json20
-rw-r--r--.sqlx/query-bddc3b0d75f6048c36630db3abb8945a49ce18fb715d249bc9d93fc7d10e817d.json62
-rw-r--r--.sqlx/query-c83af5b462071927d05664bb69067dd15b2e7602aafcfaf04aa1f41e118a1f87.json20
-rw-r--r--.sqlx/query-cda3a4a974eb986ebe26838143f6b5d16dbeb19ad13ac36dcb40851f0af238e8.json38
-rw-r--r--.sqlx/query-e2686f26f8646b4cd31beeb2060b9a6d6e0bbcb4cf8d01c48b297e6f0a950ebc.json50
-rw-r--r--.sqlx/query-e476c3fb3f2227b7421ad2075d18b090751a345ac2e90bb8c116e0fbb5dfbb99.json50
-rw-r--r--.sqlx/query-e702a0e7ff9a0a9808f8d45294ae908114b03719dc0cb237cec11f807bf757b1.json62
-rw-r--r--.sqlx/query-ea5d8528095f8885b7e31687ec44bfea55ae259cc5fd412bdc30881b98ed12f8.json20
-rw-r--r--.sqlx/query-f3a338b9e4a65856decd79ad2d2dec42949522f182a61bfb249f13ee78564179.json20
-rw-r--r--.sqlx/query-f511d351018c908bf303ae20ac429b55db93ceb5ce501917188e7b2f14975f34.json12
-rw-r--r--.sqlx/query-f6909336ab05b7ad423c7b96a0e7b12a920f9827aff2b05ee0364ff7688a38ae.json38
-rw-r--r--docs/api/channels-messages.md12
-rw-r--r--docs/api/events.md34
-rw-r--r--migrations/20241017005219_retain_deleted.sql77
-rw-r--r--src/channel/app.rs24
-rw-r--r--src/channel/history.rs5
-rw-r--r--src/channel/repo.rs179
-rw-r--r--src/channel/routes/channel/delete.rs2
-rw-r--r--src/channel/routes/channel/test.rs6
-rw-r--r--src/channel/routes/test.rs106
-rw-r--r--src/channel/snapshot.rs3
-rw-r--r--src/event/repo.rs5
-rw-r--r--src/event/routes/test.rs33
-rw-r--r--src/event/sequence.rs11
-rw-r--r--src/expire.rs2
-rw-r--r--src/login/repo.rs15
-rw-r--r--src/message/app.rs24
-rw-r--r--src/message/history.rs5
-rw-r--r--src/message/repo.rs214
-rw-r--r--src/message/routes/message.rs6
-rw-r--r--src/message/snapshot.rs4
-rw-r--r--src/test/fixtures/channel.rs23
-rw-r--r--src/test/fixtures/event.rs7
-rw-r--r--src/test/fixtures/filter.rs11
-rw-r--r--src/test/fixtures/message.rs18
-rw-r--r--src/test/fixtures/mod.rs1
-rw-r--r--src/token/repo/auth.rs5
56 files changed, 1260 insertions, 648 deletions
diff --git a/.sqlx/query-152cbd56cdce2ee7e94ae2568c112d0f6fe2aa136f0758a68f1eb66787db52f0.json b/.sqlx/query-152cbd56cdce2ee7e94ae2568c112d0f6fe2aa136f0758a68f1eb66787db52f0.json
new file mode 100644
index 0000000..2c7666f
--- /dev/null
+++ b/.sqlx/query-152cbd56cdce2ee7e94ae2568c112d0f6fe2aa136f0758a68f1eb66787db52f0.json
@@ -0,0 +1,50 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n id as \"id: Id\",\n channel.name,\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n where id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at: DateTime",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_sequence: Sequence",
+ "ordinal": 3,
+ "type_info": "Integer"
+ },
+ {
+ "name": "deleted_at?: DateTime",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_sequence?: Sequence",
+ "ordinal": 5,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ true,
+ false,
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "152cbd56cdce2ee7e94ae2568c112d0f6fe2aa136f0758a68f1eb66787db52f0"
+}
diff --git a/.sqlx/query-15ec7a8aa11908f8b2022accdde2e9e4a49e52e74c3d0e28e82a0b01a3c9887f.json b/.sqlx/query-15ec7a8aa11908f8b2022accdde2e9e4a49e52e74c3d0e28e82a0b01a3c9887f.json
new file mode 100644
index 0000000..167e768
--- /dev/null
+++ b/.sqlx/query-15ec7a8aa11908f8b2022accdde2e9e4a49e52e74c3d0e28e82a0b01a3c9887f.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n insert into message_deleted (id, deleted_at, deleted_sequence)\n values ($1, $2, $3)\n returning 1 as \"deleted: bool\"\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "deleted: bool",
+ "ordinal": 0,
+ "type_info": "Null"
+ }
+ ],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": [
+ null
+ ]
+ },
+ "hash": "15ec7a8aa11908f8b2022accdde2e9e4a49e52e74c3d0e28e82a0b01a3c9887f"
+}
diff --git a/.sqlx/query-1654b05159c27f74cb333586018161718e2a6788413bffffb252de3e1959f341.json b/.sqlx/query-1654b05159c27f74cb333586018161718e2a6788413bffffb252de3e1959f341.json
deleted file mode 100644
index cc716ed..0000000
--- a/.sqlx/query-1654b05159c27f74cb333586018161718e2a6788413bffffb252de3e1959f341.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n delete from channel\n where id = $1\n returning\n id as \"id: Id\",\n name,\n created_at as \"created_at: DateTime\",\n created_sequence as \"created_sequence: Sequence\"\n ",
- "describe": {
- "columns": [
- {
- "name": "id: Id",
- "ordinal": 0,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "created_at: DateTime",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "created_sequence: Sequence",
- "ordinal": 3,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- false,
- false,
- false,
- false
- ]
- },
- "hash": "1654b05159c27f74cb333586018161718e2a6788413bffffb252de3e1959f341"
-}
diff --git a/.sqlx/query-191255b9e55c9b36d0fd047e56e515d121964d3481e48aae3558b53a5123ce7d.json b/.sqlx/query-191255b9e55c9b36d0fd047e56e515d121964d3481e48aae3558b53a5123ce7d.json
deleted file mode 100644
index fe443f9..0000000
--- a/.sqlx/query-191255b9e55c9b36d0fd047e56e515d121964d3481e48aae3558b53a5123ce7d.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n select\n channel as \"channel: channel::Id\",\n sender as \"sender: login::Id\",\n id as \"id: Id\",\n body,\n sent_at as \"sent_at: DateTime\",\n sent_sequence as \"sent_sequence: Sequence\"\n from message\n where coalesce(sent_sequence <= $2, true)\n order by sent_sequence\n ",
- "describe": {
- "columns": [
- {
- "name": "channel: channel::Id",
- "ordinal": 0,
- "type_info": "Text"
- },
- {
- "name": "sender: login::Id",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "id: Id",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "body",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "sent_at: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "sent_sequence: Sequence",
- "ordinal": 5,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "191255b9e55c9b36d0fd047e56e515d121964d3481e48aae3558b53a5123ce7d"
-}
diff --git a/.sqlx/query-33f9a143409e6f436ed6b64abfbb9e06a5889460743202ca7956acabf006843e.json b/.sqlx/query-33f9a143409e6f436ed6b64abfbb9e06a5889460743202ca7956acabf006843e.json
deleted file mode 100644
index 1480953..0000000
--- a/.sqlx/query-33f9a143409e6f436ed6b64abfbb9e06a5889460743202ca7956acabf006843e.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n delete from message\n where\n id = $1\n returning 1 as \"deleted: i64\"\n ",
- "describe": {
- "columns": [
- {
- "name": "deleted: i64",
- "ordinal": 0,
- "type_info": "Null"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- null
- ]
- },
- "hash": "33f9a143409e6f436ed6b64abfbb9e06a5889460743202ca7956acabf006843e"
-}
diff --git a/.sqlx/query-34d3bc2b18bc0813cbaafc1ef99603f820d3b391f212c2c0883506a53ee757ca.json b/.sqlx/query-34d3bc2b18bc0813cbaafc1ef99603f820d3b391f212c2c0883506a53ee757ca.json
new file mode 100644
index 0000000..63ff012
--- /dev/null
+++ b/.sqlx/query-34d3bc2b18bc0813cbaafc1ef99603f820d3b391f212c2c0883506a53ee757ca.json
@@ -0,0 +1,62 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n id as \"id: Id\",\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n message.body,\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.sent_at < $1\n and deleted.id is null\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "channel: channel::Id",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "sender: login::Id",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_at: DateTime",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_sequence: Sequence",
+ "ordinal": 4,
+ "type_info": "Integer"
+ },
+ {
+ "name": "body",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_at?: DateTime",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_sequence?: Sequence",
+ "ordinal": 7,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "34d3bc2b18bc0813cbaafc1ef99603f820d3b391f212c2c0883506a53ee757ca"
+}
diff --git a/.sqlx/query-400de8e1dca27c48362b060c5b16e9ae908c4ea30e2d682d59d4720699b8de91.json b/.sqlx/query-400de8e1dca27c48362b060c5b16e9ae908c4ea30e2d682d59d4720699b8de91.json
new file mode 100644
index 0000000..0caa950
--- /dev/null
+++ b/.sqlx/query-400de8e1dca27c48362b060c5b16e9ae908c4ea30e2d682d59d4720699b8de91.json
@@ -0,0 +1,62 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n id as \"id: Id\",\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n message.body,\n deleted.deleted_at as \"deleted_at: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where coalesce(message.sent_sequence > $1, true)\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "channel: channel::Id",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "sender: login::Id",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_at: DateTime",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_sequence: Sequence",
+ "ordinal": 4,
+ "type_info": "Integer"
+ },
+ {
+ "name": "body",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_at: DateTime",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_sequence: Sequence",
+ "ordinal": 7,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true
+ ]
+ },
+ "hash": "400de8e1dca27c48362b060c5b16e9ae908c4ea30e2d682d59d4720699b8de91"
+}
diff --git a/.sqlx/query-487092b18b01005fdb446c11f4726612141f7242decad1de5d9d5f166cb04990.json b/.sqlx/query-487092b18b01005fdb446c11f4726612141f7242decad1de5d9d5f166cb04990.json
new file mode 100644
index 0000000..c919eb2
--- /dev/null
+++ b/.sqlx/query-487092b18b01005fdb446c11f4726612141f7242decad1de5d9d5f166cb04990.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n with has_messages as (\n select channel\n from message\n group by channel\n )\n delete from channel_deleted\n where deleted_at < $1\n and id not in has_messages\n returning id as \"id: Id\"\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "487092b18b01005fdb446c11f4726612141f7242decad1de5d9d5f166cb04990"
+}
diff --git a/.sqlx/query-647c3387fd01747b6318b946d2ea19a3007558abe81a1635c64ef7960acd30b2.json b/.sqlx/query-647c3387fd01747b6318b946d2ea19a3007558abe81a1635c64ef7960acd30b2.json
new file mode 100644
index 0000000..f706f81
--- /dev/null
+++ b/.sqlx/query-647c3387fd01747b6318b946d2ea19a3007558abe81a1635c64ef7960acd30b2.json
@@ -0,0 +1,50 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n channel.id as \"id: Id\",\n channel.name,\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n left join message\n where channel.created_at < $1\n and message.id is null\n and deleted.id is null\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at: DateTime",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_sequence: Sequence",
+ "ordinal": 3,
+ "type_info": "Integer"
+ },
+ {
+ "name": "deleted_at?: DateTime",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_sequence?: Sequence",
+ "ordinal": 5,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ true,
+ false,
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "647c3387fd01747b6318b946d2ea19a3007558abe81a1635c64ef7960acd30b2"
+}
diff --git a/.sqlx/query-814400dfe985ef284db22c89d6e1a0eb2131b6581759a5028afb6a0910284803.json b/.sqlx/query-814400dfe985ef284db22c89d6e1a0eb2131b6581759a5028afb6a0910284803.json
new file mode 100644
index 0000000..4bcf6d0
--- /dev/null
+++ b/.sqlx/query-814400dfe985ef284db22c89d6e1a0eb2131b6581759a5028afb6a0910284803.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n delete from channel\n where id = $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "814400dfe985ef284db22c89d6e1a0eb2131b6581759a5028afb6a0910284803"
+}
diff --git a/.sqlx/query-836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418.json b/.sqlx/query-836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418.json
index 2ac20b6..4770b7e 100644
--- a/.sqlx/query-836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418.json
+++ b/.sqlx/query-836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418.json
@@ -43,7 +43,7 @@
false,
false,
false,
- false
+ true
]
},
"hash": "836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418"
diff --git a/.sqlx/query-8d9b83fb53e4191ee58b68e65e101d0941f2a266679209f793fd27466b9e3719.json b/.sqlx/query-8d9b83fb53e4191ee58b68e65e101d0941f2a266679209f793fd27466b9e3719.json
new file mode 100644
index 0000000..4d28006
--- /dev/null
+++ b/.sqlx/query-8d9b83fb53e4191ee58b68e65e101d0941f2a266679209f793fd27466b9e3719.json
@@ -0,0 +1,50 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n id as \"id: Id\",\n channel.name,\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n where coalesce(channel.created_sequence <= $1, true)\n order by channel.name\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at: DateTime",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_sequence: Sequence",
+ "ordinal": 3,
+ "type_info": "Integer"
+ },
+ {
+ "name": "deleted_at: DateTime",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_sequence: Sequence",
+ "ordinal": 5,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ true,
+ false,
+ false,
+ true,
+ true
+ ]
+ },
+ "hash": "8d9b83fb53e4191ee58b68e65e101d0941f2a266679209f793fd27466b9e3719"
+}
diff --git a/.sqlx/query-8e816ccef8ecee937d88ff31ca611395405093dbbed738579d715b2d623921be.json b/.sqlx/query-8e816ccef8ecee937d88ff31ca611395405093dbbed738579d715b2d623921be.json
new file mode 100644
index 0000000..5a038ac
--- /dev/null
+++ b/.sqlx/query-8e816ccef8ecee937d88ff31ca611395405093dbbed738579d715b2d623921be.json
@@ -0,0 +1,62 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n id as \"id: Id\",\n message.body,\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "channel: channel::Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "sender: login::Id",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "id: Id",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "body",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_at: DateTime",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_sequence: Sequence",
+ "ordinal": 5,
+ "type_info": "Integer"
+ },
+ {
+ "name": "deleted_at?: DateTime",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_sequence?: Sequence",
+ "ordinal": 7,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "8e816ccef8ecee937d88ff31ca611395405093dbbed738579d715b2d623921be"
+}
diff --git a/.sqlx/query-8eebba45e8267b9eebc7b1ae7e18ecb51d54d1e304dcbd847de145c0c75423a7.json b/.sqlx/query-8eebba45e8267b9eebc7b1ae7e18ecb51d54d1e304dcbd847de145c0c75423a7.json
new file mode 100644
index 0000000..0394bc5
--- /dev/null
+++ b/.sqlx/query-8eebba45e8267b9eebc7b1ae7e18ecb51d54d1e304dcbd847de145c0c75423a7.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n update message\n set body = \"\"\n where id = $1\n returning 1 as \"blanked: bool\"\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "blanked: bool",
+ "ordinal": 0,
+ "type_info": "Null"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ null
+ ]
+ },
+ "hash": "8eebba45e8267b9eebc7b1ae7e18ecb51d54d1e304dcbd847de145c0c75423a7"
+}
diff --git a/.sqlx/query-9386cdaa2cb41f5a7e19d2fc8c187294a4661c18c2d820f4379dfd82138a8f77.json b/.sqlx/query-9386cdaa2cb41f5a7e19d2fc8c187294a4661c18c2d820f4379dfd82138a8f77.json
deleted file mode 100644
index e9c3967..0000000
--- a/.sqlx/query-9386cdaa2cb41f5a7e19d2fc8c187294a4661c18c2d820f4379dfd82138a8f77.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n select\n id as \"id: Id\",\n name,\n created_at as \"created_at: DateTime\",\n created_sequence as \"created_sequence: Sequence\"\n from channel\n where coalesce(created_sequence > $1, true)\n ",
- "describe": {
- "columns": [
- {
- "name": "id: Id",
- "ordinal": 0,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "created_at: DateTime",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "created_sequence: Sequence",
- "ordinal": 3,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- false,
- false,
- false,
- false
- ]
- },
- "hash": "9386cdaa2cb41f5a7e19d2fc8c187294a4661c18c2d820f4379dfd82138a8f77"
-}
diff --git a/.sqlx/query-023b1e263b68a483704ae5f6b07e69ab792b7365f2eb2831f7a2ac13e2ecf323.json b/.sqlx/query-9da5b746de5d481eeb0755283b4094fa9dfa65dc95991f689355889606ab1c46.json
index cc23359..56a8498 100644
--- a/.sqlx/query-023b1e263b68a483704ae5f6b07e69ab792b7365f2eb2831f7a2ac13e2ecf323.json
+++ b/.sqlx/query-9da5b746de5d481eeb0755283b4094fa9dfa65dc95991f689355889606ab1c46.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n insert\n into channel (id, name, created_at, created_sequence)\n values ($1, $2, $3, $4)\n returning\n id as \"id: Id\",\n name,\n created_at as \"created_at: DateTime\",\n created_sequence as \"created_sequence: Sequence\"\n ",
+ "query": "\n insert\n into channel (id, name, created_at, created_sequence)\n values ($1, $2, $3, $4)\n returning\n id as \"id: Id\",\n name as \"name!\", -- known non-null as we just set it\n created_at as \"created_at: DateTime\",\n created_sequence as \"created_sequence: Sequence\"\n ",
"describe": {
"columns": [
{
@@ -9,7 +9,7 @@
"type_info": "Text"
},
{
- "name": "name",
+ "name": "name!",
"ordinal": 1,
"type_info": "Text"
},
@@ -29,10 +29,10 @@
},
"nullable": [
false,
- false,
+ true,
false,
false
]
},
- "hash": "023b1e263b68a483704ae5f6b07e69ab792b7365f2eb2831f7a2ac13e2ecf323"
+ "hash": "9da5b746de5d481eeb0755283b4094fa9dfa65dc95991f689355889606ab1c46"
}
diff --git a/.sqlx/query-9f83f0365b174ce61adb6d48f3b75adcb3ddf8a88439aa8ddb0c66be2f0b384e.json b/.sqlx/query-9f83f0365b174ce61adb6d48f3b75adcb3ddf8a88439aa8ddb0c66be2f0b384e.json
deleted file mode 100644
index c8ce115..0000000
--- a/.sqlx/query-9f83f0365b174ce61adb6d48f3b75adcb3ddf8a88439aa8ddb0c66be2f0b384e.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n select\n channel as \"channel: channel::Id\",\n sender as \"sender: login::Id\",\n id as \"id: Id\",\n body,\n sent_at as \"sent_at: DateTime\",\n sent_sequence as \"sent_sequence: Sequence\"\n from message\n where id = $1\n ",
- "describe": {
- "columns": [
- {
- "name": "channel: channel::Id",
- "ordinal": 0,
- "type_info": "Text"
- },
- {
- "name": "sender: login::Id",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "id: Id",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "body",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "sent_at: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "sent_sequence: Sequence",
- "ordinal": 5,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "9f83f0365b174ce61adb6d48f3b75adcb3ddf8a88439aa8ddb0c66be2f0b384e"
-}
diff --git a/.sqlx/query-afe4404b44bd8ce6c9a51aafabe3524caa9882d0cba0dc4359eb62cf8154c92d.json b/.sqlx/query-afe4404b44bd8ce6c9a51aafabe3524caa9882d0cba0dc4359eb62cf8154c92d.json
deleted file mode 100644
index cc8a074..0000000
--- a/.sqlx/query-afe4404b44bd8ce6c9a51aafabe3524caa9882d0cba0dc4359eb62cf8154c92d.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n select\n channel as \"channel: channel::Id\",\n sender as \"sender: login::Id\",\n id as \"id: Id\",\n body,\n sent_at as \"sent_at: DateTime\",\n sent_sequence as \"sent_sequence: Sequence\"\n from message\n where channel = $1\n and coalesce(sent_sequence <= $2, true)\n order by sent_sequence\n ",
- "describe": {
- "columns": [
- {
- "name": "channel: channel::Id",
- "ordinal": 0,
- "type_info": "Text"
- },
- {
- "name": "sender: login::Id",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "id: Id",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "body",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "sent_at: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "sent_sequence: Sequence",
- "ordinal": 5,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 2
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "afe4404b44bd8ce6c9a51aafabe3524caa9882d0cba0dc4359eb62cf8154c92d"
-}
diff --git a/.sqlx/query-b1ba724a62135a6fbcb7ad314ba09dbe4e42fd49b2007e47bcfb07b5ba9001b0.json b/.sqlx/query-b1ba724a62135a6fbcb7ad314ba09dbe4e42fd49b2007e47bcfb07b5ba9001b0.json
new file mode 100644
index 0000000..5825ce8
--- /dev/null
+++ b/.sqlx/query-b1ba724a62135a6fbcb7ad314ba09dbe4e42fd49b2007e47bcfb07b5ba9001b0.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n insert into channel_deleted (id, deleted_at, deleted_sequence)\n values ($1, $2, $3)\n returning 1 as \"deleted: bool\"\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "deleted: bool",
+ "ordinal": 0,
+ "type_info": "Null"
+ }
+ ],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": [
+ null
+ ]
+ },
+ "hash": "b1ba724a62135a6fbcb7ad314ba09dbe4e42fd49b2007e47bcfb07b5ba9001b0"
+}
diff --git a/.sqlx/query-b7e05e2b2eb5484c954bcedf5de8c7d9ad11bb09a0d0d243181d97c79d771071.json b/.sqlx/query-b7e05e2b2eb5484c954bcedf5de8c7d9ad11bb09a0d0d243181d97c79d771071.json
deleted file mode 100644
index b82727f..0000000
--- a/.sqlx/query-b7e05e2b2eb5484c954bcedf5de8c7d9ad11bb09a0d0d243181d97c79d771071.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n select\n channel.id as \"id: Id\"\n from channel\n left join message\n where created_at < $1\n and message.id is null\n ",
- "describe": {
- "columns": [
- {
- "name": "id: Id",
- "ordinal": 0,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- false
- ]
- },
- "hash": "b7e05e2b2eb5484c954bcedf5de8c7d9ad11bb09a0d0d243181d97c79d771071"
-}
diff --git a/.sqlx/query-bddc3b0d75f6048c36630db3abb8945a49ce18fb715d249bc9d93fc7d10e817d.json b/.sqlx/query-bddc3b0d75f6048c36630db3abb8945a49ce18fb715d249bc9d93fc7d10e817d.json
new file mode 100644
index 0000000..abc1851
--- /dev/null
+++ b/.sqlx/query-bddc3b0d75f6048c36630db3abb8945a49ce18fb715d249bc9d93fc7d10e817d.json
@@ -0,0 +1,62 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n id as \"id: Id\",\n message.body,\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where coalesce(message.sent_sequence <= $2, true)\n order by message.sent_sequence\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "channel: channel::Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "sender: login::Id",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "id: Id",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "body",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_at: DateTime",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_sequence: Sequence",
+ "ordinal": 5,
+ "type_info": "Integer"
+ },
+ {
+ "name": "deleted_at: DateTime",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_sequence: Sequence",
+ "ordinal": 7,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ true,
+ true
+ ]
+ },
+ "hash": "bddc3b0d75f6048c36630db3abb8945a49ce18fb715d249bc9d93fc7d10e817d"
+}
diff --git a/.sqlx/query-c83af5b462071927d05664bb69067dd15b2e7602aafcfaf04aa1f41e118a1f87.json b/.sqlx/query-c83af5b462071927d05664bb69067dd15b2e7602aafcfaf04aa1f41e118a1f87.json
new file mode 100644
index 0000000..01bb071
--- /dev/null
+++ b/.sqlx/query-c83af5b462071927d05664bb69067dd15b2e7602aafcfaf04aa1f41e118a1f87.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n update channel\n set name = null\n where id = $1\n returning 1 as \"updated: bool\"\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "updated: bool",
+ "ordinal": 0,
+ "type_info": "Null"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ null
+ ]
+ },
+ "hash": "c83af5b462071927d05664bb69067dd15b2e7602aafcfaf04aa1f41e118a1f87"
+}
diff --git a/.sqlx/query-cda3a4a974eb986ebe26838143f6b5d16dbeb19ad13ac36dcb40851f0af238e8.json b/.sqlx/query-cda3a4a974eb986ebe26838143f6b5d16dbeb19ad13ac36dcb40851f0af238e8.json
deleted file mode 100644
index bce6a88..0000000
--- a/.sqlx/query-cda3a4a974eb986ebe26838143f6b5d16dbeb19ad13ac36dcb40851f0af238e8.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n select\n id as \"id: Id\",\n name,\n created_at as \"created_at: DateTime\",\n created_sequence as \"created_sequence: Sequence\"\n from channel\n where coalesce(created_sequence <= $1, true)\n order by channel.name\n ",
- "describe": {
- "columns": [
- {
- "name": "id: Id",
- "ordinal": 0,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "created_at: DateTime",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "created_sequence: Sequence",
- "ordinal": 3,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- false,
- false,
- false,
- false
- ]
- },
- "hash": "cda3a4a974eb986ebe26838143f6b5d16dbeb19ad13ac36dcb40851f0af238e8"
-}
diff --git a/.sqlx/query-e2686f26f8646b4cd31beeb2060b9a6d6e0bbcb4cf8d01c48b297e6f0a950ebc.json b/.sqlx/query-e2686f26f8646b4cd31beeb2060b9a6d6e0bbcb4cf8d01c48b297e6f0a950ebc.json
new file mode 100644
index 0000000..1883035
--- /dev/null
+++ b/.sqlx/query-e2686f26f8646b4cd31beeb2060b9a6d6e0bbcb4cf8d01c48b297e6f0a950ebc.json
@@ -0,0 +1,50 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n id as \"id: Id\",\n channel.name,\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n where coalesce(channel.created_sequence > $1, true)\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at: DateTime",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_sequence: Sequence",
+ "ordinal": 3,
+ "type_info": "Integer"
+ },
+ {
+ "name": "deleted_at: DateTime",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_sequence: Sequence",
+ "ordinal": 5,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ true,
+ false,
+ false,
+ true,
+ true
+ ]
+ },
+ "hash": "e2686f26f8646b4cd31beeb2060b9a6d6e0bbcb4cf8d01c48b297e6f0a950ebc"
+}
diff --git a/.sqlx/query-e476c3fb3f2227b7421ad2075d18b090751a345ac2e90bb8c116e0fbb5dfbb99.json b/.sqlx/query-e476c3fb3f2227b7421ad2075d18b090751a345ac2e90bb8c116e0fbb5dfbb99.json
deleted file mode 100644
index 821bd8f..0000000
--- a/.sqlx/query-e476c3fb3f2227b7421ad2075d18b090751a345ac2e90bb8c116e0fbb5dfbb99.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n select\n channel as \"channel: channel::Id\",\n sender as \"sender: login::Id\",\n id as \"id: Id\",\n body,\n sent_at as \"sent_at: DateTime\",\n sent_sequence as \"sent_sequence: Sequence\"\n from message\n where coalesce(message.sent_sequence > $1, true)\n ",
- "describe": {
- "columns": [
- {
- "name": "channel: channel::Id",
- "ordinal": 0,
- "type_info": "Text"
- },
- {
- "name": "sender: login::Id",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "id: Id",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "body",
- "ordinal": 3,
- "type_info": "Text"
- },
- {
- "name": "sent_at: DateTime",
- "ordinal": 4,
- "type_info": "Text"
- },
- {
- "name": "sent_sequence: Sequence",
- "ordinal": 5,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "e476c3fb3f2227b7421ad2075d18b090751a345ac2e90bb8c116e0fbb5dfbb99"
-}
diff --git a/.sqlx/query-e702a0e7ff9a0a9808f8d45294ae908114b03719dc0cb237cec11f807bf757b1.json b/.sqlx/query-e702a0e7ff9a0a9808f8d45294ae908114b03719dc0cb237cec11f807bf757b1.json
new file mode 100644
index 0000000..ba35bb9
--- /dev/null
+++ b/.sqlx/query-e702a0e7ff9a0a9808f8d45294ae908114b03719dc0cb237cec11f807bf757b1.json
@@ -0,0 +1,62 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n id as \"id: Id\",\n message.body,\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.channel = $1\n and deleted.id is null\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "channel: channel::Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "sender: login::Id",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "id: Id",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "body",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_at: DateTime",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "sent_sequence: Sequence",
+ "ordinal": 5,
+ "type_info": "Integer"
+ },
+ {
+ "name": "deleted_at: DateTime",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_sequence: Sequence",
+ "ordinal": 7,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ true,
+ true
+ ]
+ },
+ "hash": "e702a0e7ff9a0a9808f8d45294ae908114b03719dc0cb237cec11f807bf757b1"
+}
diff --git a/.sqlx/query-ea5d8528095f8885b7e31687ec44bfea55ae259cc5fd412bdc30881b98ed12f8.json b/.sqlx/query-ea5d8528095f8885b7e31687ec44bfea55ae259cc5fd412bdc30881b98ed12f8.json
new file mode 100644
index 0000000..68b7155
--- /dev/null
+++ b/.sqlx/query-ea5d8528095f8885b7e31687ec44bfea55ae259cc5fd412bdc30881b98ed12f8.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n delete from message_deleted\n where deleted_at < $1\n returning id as \"id: Id\"\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "ea5d8528095f8885b7e31687ec44bfea55ae259cc5fd412bdc30881b98ed12f8"
+}
diff --git a/.sqlx/query-f3a338b9e4a65856decd79ad2d2dec42949522f182a61bfb249f13ee78564179.json b/.sqlx/query-f3a338b9e4a65856decd79ad2d2dec42949522f182a61bfb249f13ee78564179.json
deleted file mode 100644
index 92a64a3..0000000
--- a/.sqlx/query-f3a338b9e4a65856decd79ad2d2dec42949522f182a61bfb249f13ee78564179.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n select\n id as \"message: Id\"\n from message\n where sent_at < $1\n ",
- "describe": {
- "columns": [
- {
- "name": "message: Id",
- "ordinal": 0,
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- false
- ]
- },
- "hash": "f3a338b9e4a65856decd79ad2d2dec42949522f182a61bfb249f13ee78564179"
-}
diff --git a/.sqlx/query-f511d351018c908bf303ae20ac429b55db93ceb5ce501917188e7b2f14975f34.json b/.sqlx/query-f511d351018c908bf303ae20ac429b55db93ceb5ce501917188e7b2f14975f34.json
new file mode 100644
index 0000000..69fdf03
--- /dev/null
+++ b/.sqlx/query-f511d351018c908bf303ae20ac429b55db93ceb5ce501917188e7b2f14975f34.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n delete from message\n where id = $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "f511d351018c908bf303ae20ac429b55db93ceb5ce501917188e7b2f14975f34"
+}
diff --git a/.sqlx/query-f6909336ab05b7ad423c7b96a0e7b12a920f9827aff2b05ee0364ff7688a38ae.json b/.sqlx/query-f6909336ab05b7ad423c7b96a0e7b12a920f9827aff2b05ee0364ff7688a38ae.json
deleted file mode 100644
index ded48e1..0000000
--- a/.sqlx/query-f6909336ab05b7ad423c7b96a0e7b12a920f9827aff2b05ee0364ff7688a38ae.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n select\n id as \"id: Id\",\n name,\n created_at as \"created_at: DateTime\",\n created_sequence as \"created_sequence: Sequence\"\n from channel\n where id = $1\n ",
- "describe": {
- "columns": [
- {
- "name": "id: Id",
- "ordinal": 0,
- "type_info": "Text"
- },
- {
- "name": "name",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "created_at: DateTime",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "created_sequence: Sequence",
- "ordinal": 3,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 1
- },
- "nullable": [
- false,
- false,
- false,
- false
- ]
- },
- "hash": "f6909336ab05b7ad423c7b96a0e7b12a920f9827aff2b05ee0364ff7688a38ae"
-}
diff --git a/docs/api/channels-messages.md b/docs/api/channels-messages.md
index d032ac1..99a525e 100644
--- a/docs/api/channels-messages.md
+++ b/docs/api/channels-messages.md
@@ -8,7 +8,7 @@ stateDiagram-v2
[*] --> Active : POST /api/channels
Active --> Deleted : DELETE /api/channels/C1234
Active --> Deleted : Expiry
- Deleted --> [*]
+ Deleted --> [*] : Purge
```
```mermaid
@@ -19,14 +19,19 @@ stateDiagram-v2
[*] --> Sent : POST /api/channels/C1234
Sent --> Deleted : DELETE /api/messages/Mabcd
Sent --> Deleted : Expiry
- Deleted --> [*]
+ Deleted --> [*] : Purge
```
Messages allow logins to communicate with one another. Channels are the conversations to which those messages are sent.
Every channel has a unique name, chosen when the channel is created.
-Both channels and messages expire after a time, to prevent the service from consuming unlimited amounts of storage. Messages expire 90 days after being sent. Channels expire 90 days after the last message sent to them, or after creation if no messages are sent in that time.
+
+## Expiry and purging
+
+Both channels and messages expire after a time. Messages expire 90 days after being sent. Channels expire 90 days after the last message sent to them, or after creation if no messages are sent in that time.
+
+Deleted channels and messages, including those that have expired, are temporarily retained by the service, to allow clients that are not connected to receive the corresponding deletion [events](./events.md). To limit storage growth, deleted channels and messages are purged from the service seven days after they were deleted.
## `POST /api/channels`
@@ -103,6 +108,7 @@ This endpoint will respond with a status of `202 Accepted` when successful. The
This endpoint will respond with a status of `404 Not Found` if the channel ID is not valid.
+
## `DELETE /api/channels/:id`
Deletes a channel (and all messages in it).
diff --git a/docs/api/events.md b/docs/api/events.md
index 9fe9ca9..2f48df1 100644
--- a/docs/api/events.md
+++ b/docs/api/events.md
@@ -133,11 +133,16 @@ Sent whenever a new channel is created.
These events have the `event` field set to `"created"`. They include the following additional fields:
-| Field | Type | Description |
-|:-------|:----------|:--|
-| `at` | timestamp | The moment the channel was created. |
-| `id` | string | A unique identifier for the newly-created channel. This can be used to associate the channel with other events, or to make API calls targeting the channel. |
-| `name` | string | The channel's name. |
+| Field | Type | Description |
+|:-------------|:--------------------|:--|
+| `at` | timestamp | The moment the channel was created. |
+| `id` | string | A unique identifier for the newly-created channel. This can be used to associate the channel with other events, or to make API calls targeting the channel. |
+| `name` | string | The channel's name. |
+| `deleted_at` | timestamp, optional | If set, the moment the channel was deleted. |
+
+When a channel is deleted or expires, the `"created"` event is replaced with a tombstone `"created"` event, so that the original channel cannot be trivially recovered from the event stream. Tombstone events have a `deleted_at` field, and a `name` of `""`. Tombstone events for channels use an empty string as the name, and not `null` or with the `name` field removed entirely, to simplify client development. While clients _should_ treat deleted channels specially, for example by rendering them as "channel deleted" markers, they don't have to be - they make sense if interpreted as channels with empty names, too.
+
+Once a deleted channel is [purged](./channels-messages.md#expiry-and-purging), these tombstone events are removed from the event stream.
### Channel deleted
@@ -184,13 +189,18 @@ Sent whenever a message is sent by a client.
These events have the `event` field set to `"sent"`. They include the following additional fields:
-| Field | Type | Description |
-|:----------|:----------|:--|
-| `at` | timestamp | The moment the message was sent. |
-| `channel` | string | The ID of the channel the message was sent to. |
-| `sender` | string | The ID of the login that sent the message. |
-| `id` | string | A unique identifier for the message. This can be used to associate the message with other events, or to make API calls targeting the message. |
-| `body` | string | The text of the message. |
+| Field | Type | Description |
+|:-------------|:--------------------|:--|
+| `at` | timestamp | The moment the message was sent. |
+| `channel` | string | The ID of the channel the message was sent to. |
+| `sender` | string | The ID of the login that sent the message. |
+| `id` | string | A unique identifier for the message. This can be used to associate the message with other events, or to make API calls targeting the message. |
+| `body` | string | The text of the message. |
+| `deleted_at` | timestamp, optional | If set, the moment the message was deleted. |
+
+When a message is deleted or expires, the `"sent"` event is replaced with a tombstone `"sent"` event, so that the original message cannot be trivially recovered from the event stream. Tombstone events have a `deleted_at` field, and a `body` of `""`. Tombstone events for messages use an empty string as the `body`, and not `null` or with the `body` field removed entirely, to simplify client development. While clients _should_ treat deleted messages specially, for example by rendering them as "message deleted" markers, they don't have to be - they make sense if interpreted as messages with empty bodies, too.
+
+Once a deleted message is [purged](./channels-messages.md#expiry-and-purging), these tombstone events are removed from the event stream.
### Message deleted
diff --git a/migrations/20241017005219_retain_deleted.sql b/migrations/20241017005219_retain_deleted.sql
new file mode 100644
index 0000000..aed87f4
--- /dev/null
+++ b/migrations/20241017005219_retain_deleted.sql
@@ -0,0 +1,77 @@
+alter table channel
+rename to old_channel;
+
+alter table message
+rename to old_message;
+
+create table channel (
+ id text
+ not null
+ primary key,
+ name text
+ null
+ unique,
+ created_sequence bigint
+ unique
+ not null,
+ created_at text
+ not null
+);
+
+insert into channel (id, name, created_sequence, created_at)
+select id, name, created_sequence, created_at from old_channel;
+
+create table channel_deleted (
+ id text
+ not null
+ primary key
+ references channel (id),
+ deleted_sequence bigint
+ unique
+ not null,
+ deleted_at text
+ not null
+);
+
+create table message (
+ id text
+ not null
+ primary key,
+ channel text
+ not null
+ references channel (id),
+ sender text
+ not null
+ references login (id),
+ sent_sequence bigint
+ unique
+ not null,
+ sent_at text
+ not null,
+ body text
+ null
+);
+
+insert into message (id, channel, sender, sent_sequence, sent_at, body)
+select id, channel, sender, sent_sequence, sent_at, body from old_message;
+
+create table message_deleted (
+ id text
+ not null
+ primary key
+ references message (id),
+ deleted_sequence bigint
+ unique
+ not null,
+ deleted_at text
+ not null
+);
+
+drop table old_message;
+drop table old_channel;
+
+create index message_sent_at
+on message (sent_at);
+
+create index channel_created_at
+on channel (created_at);
diff --git a/src/channel/app.rs b/src/channel/app.rs
index 46eaba8..75c662d 100644
--- a/src/channel/app.rs
+++ b/src/channel/app.rs
@@ -43,7 +43,7 @@ impl<'a> Channels<'a> {
let channel = tx.channels().by_id(channel).await.optional()?;
tx.commit().await?;
- Ok(channel.iter().flat_map(History::events).collect())
+ Ok(channel.as_ref().and_then(History::as_snapshot))
}
pub async fn delete(&self, channel: &Id, deleted_at: &DateTime) -> Result<(), Error> {
@@ -54,13 +54,16 @@ impl<'a> Channels<'a> {
.by_id(channel)
.await
.not_found(|| Error::NotFound(channel.clone()))?;
+ channel
+ .as_snapshot()
+ .ok_or_else(|| Error::Deleted(channel.id().clone()))?;
let mut events = Vec::new();
- let messages = tx.messages().in_channel(&channel, None).await?;
+ let messages = tx.messages().live(&channel).await?;
for message in messages {
let deleted = tx.sequence().next(deleted_at).await?;
- let message = tx.messages().delete(message.id(), &deleted).await?;
+ let message = tx.messages().delete(&message, &deleted).await?;
events.extend(
message
.events()
@@ -70,7 +73,7 @@ impl<'a> Channels<'a> {
}
let deleted = tx.sequence().next(deleted_at).await?;
- let channel = tx.channels().delete(channel.id(), &deleted).await?;
+ let channel = tx.channels().delete(&channel, &deleted).await?;
events.extend(
channel
.events()
@@ -115,6 +118,17 @@ impl<'a> Channels<'a> {
Ok(())
}
+
+ pub async fn purge(&self, relative_to: &DateTime) -> Result<(), sqlx::Error> {
+ // Somewhat arbitrarily, purge after 7 days.
+ let purge_at = relative_to.to_owned() - TimeDelta::days(7);
+
+ let mut tx = self.db.begin().await?;
+ tx.channels().purge(&purge_at).await?;
+ tx.commit().await?;
+
+ Ok(())
+ }
}
#[derive(Debug, thiserror::Error)]
@@ -129,6 +143,8 @@ pub enum CreateError {
pub enum Error {
#[error("channel {0} not found")]
NotFound(Id),
+ #[error("channel {0} deleted")]
+ Deleted(Id),
#[error(transparent)]
Database(#[from] sqlx::Error),
}
diff --git a/src/channel/history.rs b/src/channel/history.rs
index 78b3437..4b9fcc7 100644
--- a/src/channel/history.rs
+++ b/src/channel/history.rs
@@ -31,6 +31,11 @@ impl History {
.filter(Sequence::up_to(resume_point.into()))
.collect()
}
+
+ // Snapshot of this channel as of all events recorded in this history.
+ pub fn as_snapshot(&self) -> Option<Channel> {
+ self.events().collect()
+ }
}
// Event factories
diff --git a/src/channel/repo.rs b/src/channel/repo.rs
index 2f57581..1cd1c80 100644
--- a/src/channel/repo.rs
+++ b/src/channel/repo.rs
@@ -28,7 +28,7 @@ impl<'c> Channels<'c> {
values ($1, $2, $3, $4)
returning
id as "id: Id",
- name,
+ name as "name!", -- known non-null as we just set it
created_at as "created_at: DateTime",
created_sequence as "created_sequence: Sequence"
"#,
@@ -41,11 +41,9 @@ impl<'c> Channels<'c> {
channel: Channel {
id: row.id,
name: row.name,
+ deleted_at: None,
},
- created: Instant {
- at: row.created_at,
- sequence: row.created_sequence,
- },
+ created: Instant::new(row.created_at, row.created_sequence),
deleted: None,
})
.fetch_one(&mut *self.0)
@@ -59,10 +57,14 @@ impl<'c> Channels<'c> {
r#"
select
id as "id: Id",
- name,
- created_at as "created_at: DateTime",
- created_sequence as "created_sequence: Sequence"
+ channel.name,
+ channel.created_at as "created_at: DateTime",
+ channel.created_sequence as "created_sequence: Sequence",
+ deleted.deleted_at as "deleted_at?: DateTime",
+ deleted.deleted_sequence as "deleted_sequence?: Sequence"
from channel
+ left join channel_deleted as deleted
+ using (id)
where id = $1
"#,
channel,
@@ -70,13 +72,11 @@ impl<'c> Channels<'c> {
.map(|row| History {
channel: Channel {
id: row.id,
- name: row.name,
+ name: row.name.unwrap_or_default(),
+ deleted_at: row.deleted_at,
},
- created: Instant {
- at: row.created_at,
- sequence: row.created_sequence,
- },
- deleted: None,
+ created: Instant::new(row.created_at, row.created_sequence),
+ deleted: Instant::optional(row.deleted_at, row.deleted_sequence),
})
.fetch_one(&mut *self.0)
.await?;
@@ -89,11 +89,15 @@ impl<'c> Channels<'c> {
r#"
select
id as "id: Id",
- name,
- created_at as "created_at: DateTime",
- created_sequence as "created_sequence: Sequence"
+ channel.name,
+ channel.created_at as "created_at: DateTime",
+ channel.created_sequence as "created_sequence: Sequence",
+ deleted.deleted_at as "deleted_at: DateTime",
+ deleted.deleted_sequence as "deleted_sequence: Sequence"
from channel
- where coalesce(created_sequence <= $1, true)
+ left join channel_deleted as deleted
+ using (id)
+ where coalesce(channel.created_sequence <= $1, true)
order by channel.name
"#,
resume_at,
@@ -101,13 +105,11 @@ impl<'c> Channels<'c> {
.map(|row| History {
channel: Channel {
id: row.id,
- name: row.name,
+ name: row.name.unwrap_or_default(),
+ deleted_at: row.deleted_at,
},
- created: Instant {
- at: row.created_at,
- sequence: row.created_sequence,
- },
- deleted: None,
+ created: Instant::new(row.created_at, row.created_sequence),
+ deleted: Instant::optional(row.deleted_at, row.deleted_sequence),
})
.fetch_all(&mut *self.0)
.await?;
@@ -123,24 +125,26 @@ impl<'c> Channels<'c> {
r#"
select
id as "id: Id",
- name,
- created_at as "created_at: DateTime",
- created_sequence as "created_sequence: Sequence"
+ channel.name,
+ channel.created_at as "created_at: DateTime",
+ channel.created_sequence as "created_sequence: Sequence",
+ deleted.deleted_at as "deleted_at: DateTime",
+ deleted.deleted_sequence as "deleted_sequence: Sequence"
from channel
- where coalesce(created_sequence > $1, true)
+ left join channel_deleted as deleted
+ using (id)
+ where coalesce(channel.created_sequence > $1, true)
"#,
resume_at,
)
.map(|row| History {
channel: Channel {
id: row.id,
- name: row.name,
+ name: row.name.unwrap_or_default(),
+ deleted_at: row.deleted_at,
},
- created: Instant {
- at: row.created_at,
- sequence: row.created_sequence,
- },
- deleted: None,
+ created: Instant::new(row.created_at, row.created_sequence),
+ deleted: Instant::optional(row.deleted_at, row.deleted_sequence),
})
.fetch_all(&mut *self.0)
.await?;
@@ -150,50 +154,109 @@ impl<'c> Channels<'c> {
pub async fn delete(
&mut self,
- channel: &Id,
+ channel: &History,
deleted: &Instant,
) -> Result<History, sqlx::Error> {
- let channel = sqlx::query!(
+ let id = channel.id();
+ sqlx::query_scalar!(
r#"
- delete from channel
+ insert into channel_deleted (id, deleted_at, deleted_sequence)
+ values ($1, $2, $3)
+ returning 1 as "deleted: bool"
+ "#,
+ id,
+ deleted.at,
+ deleted.sequence,
+ )
+ .fetch_one(&mut *self.0)
+ .await?;
+
+ // Small social responsibility hack here: when a channel is deleted, its name is
+ // retconned to have been the empty string. Someone reading the event stream
+ // afterwards, or looking at channels via the API, cannot retrieve the
+ // "deleted" channel's information by ignoring the deletion event.
+ //
+ // This also avoids the need for a separate name reservation table to ensure that live channels have unique names, since the `channel` table's name field is unique over non-null values.
+ sqlx::query_scalar!(
+ r#"
+ update channel
+ set name = null
where id = $1
- returning
- id as "id: Id",
- name,
- created_at as "created_at: DateTime",
- created_sequence as "created_sequence: Sequence"
+ returning 1 as "updated: bool"
"#,
- channel,
+ id,
)
- .map(|row| History {
- channel: Channel {
- id: row.id,
- name: row.name,
- },
- created: Instant {
- at: row.created_at,
- sequence: row.created_sequence,
- },
- deleted: Some(*deleted),
- })
.fetch_one(&mut *self.0)
.await?;
+ let channel = self.by_id(id).await?;
+
Ok(channel)
}
- pub async fn expired(&mut self, expired_at: &DateTime) -> Result<Vec<Id>, sqlx::Error> {
+ pub async fn purge(&mut self, purge_at: &DateTime) -> Result<(), sqlx::Error> {
let channels = sqlx::query_scalar!(
r#"
+ with has_messages as (
+ select channel
+ from message
+ group by channel
+ )
+ delete from channel_deleted
+ where deleted_at < $1
+ and id not in has_messages
+ returning id as "id: Id"
+ "#,
+ purge_at,
+ )
+ .fetch_all(&mut *self.0)
+ .await?;
+
+ for channel in channels {
+ // Wanted: a way to batch these up into one query.
+ sqlx::query!(
+ r#"
+ delete from channel
+ where id = $1
+ "#,
+ channel,
+ )
+ .execute(&mut *self.0)
+ .await?;
+ }
+
+ Ok(())
+ }
+
+ pub async fn expired(&mut self, expired_at: &DateTime) -> Result<Vec<History>, sqlx::Error> {
+ let channels = sqlx::query!(
+ r#"
select
- channel.id as "id: Id"
+ channel.id as "id: Id",
+ channel.name,
+ channel.created_at as "created_at: DateTime",
+ channel.created_sequence as "created_sequence: Sequence",
+ deleted.deleted_at as "deleted_at?: DateTime",
+ deleted.deleted_sequence as "deleted_sequence?: Sequence"
from channel
- left join message
- where created_at < $1
+ left join channel_deleted as deleted
+ using (id)
+ left join message
+ where channel.created_at < $1
and message.id is null
+ and deleted.id is null
"#,
expired_at,
)
+ .map(|row| History {
+ channel: Channel {
+ id: row.id,
+ name: row.name.unwrap_or_default(),
+ deleted_at: row.deleted_at,
+ },
+ created: Instant::new(row.created_at, row.created_sequence),
+ deleted: Instant::optional(row.deleted_at, row.deleted_sequence),
+ })
.fetch_all(&mut *self.0)
.await?;
diff --git a/src/channel/routes/channel/delete.rs b/src/channel/routes/channel/delete.rs
index efac0c0..5f40ddf 100644
--- a/src/channel/routes/channel/delete.rs
+++ b/src/channel/routes/channel/delete.rs
@@ -32,7 +32,7 @@ impl IntoResponse for Error {
let Self(error) = self;
#[allow(clippy::match_wildcard_for_single_variants)]
match error {
- app::Error::NotFound(_) => NotFound(error).into_response(),
+ app::Error::NotFound(_) | app::Error::Deleted(_) => NotFound(error).into_response(),
other => Internal::from(other).into_response(),
}
}
diff --git a/src/channel/routes/channel/test.rs b/src/channel/routes/channel/test.rs
index bc02b20..c6541cd 100644
--- a/src/channel/routes/channel/test.rs
+++ b/src/channel/routes/channel/test.rs
@@ -4,7 +4,7 @@ use futures::stream::StreamExt;
use super::post;
use crate::{
channel,
- event::{self, Sequenced},
+ event::Sequenced,
message::{self, app::SendError},
test::fixtures::{self, future::Immediately as _},
};
@@ -45,7 +45,7 @@ async fn messages_in_order() {
.subscribe(None)
.await
.expect("subscribing to a valid channel")
- .filter(fixtures::filter::messages())
+ .filter_map(fixtures::message::events)
.take(requests.len());
let events = events.collect::<Vec<_>>().immediately().await;
@@ -54,7 +54,7 @@ async fn messages_in_order() {
assert_eq!(*sent_at, event.at());
assert!(matches!(
event,
- event::Event::Message(message::Event::Sent(event))
+ message::Event::Sent(event)
if event.message.sender == sender.id
&& event.message.body == message
));
diff --git a/src/channel/routes/test.rs b/src/channel/routes/test.rs
index 81f1465..ffd8484 100644
--- a/src/channel/routes/test.rs
+++ b/src/channel/routes/test.rs
@@ -1,10 +1,11 @@
+use std::future;
+
use axum::extract::{Json, State};
use futures::stream::StreamExt as _;
use super::post;
use crate::{
- channel::{self, app},
- event,
+ channel::app,
test::fixtures::{self, future::Immediately as _},
};
@@ -19,29 +20,35 @@ async fn new_channel() {
let name = fixtures::channel::propose();
let request = post::Request { name: name.clone() };
- let Json(response_channel) =
- post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
- .await
- .expect("new channel in an empty app");
+ let Json(response) = post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
+ .await
+ .expect("creating a channel in an empty app succeeds");
// Verify the structure of the response
- assert_eq!(name, response_channel.name);
+ assert_eq!(name, response.name);
// Verify the semantics
let snapshot = app.boot().snapshot().await.expect("boot always succeeds");
- assert!(snapshot
- .channels
- .iter()
- .any(|channel| channel.name == response_channel.name && channel.id == response_channel.id));
+ assert!(snapshot.channels.iter().any(|channel| channel == &response));
+
+ let channel = app
+ .channels()
+ .get(&response.id)
+ .await
+ .expect("searching for channels by ID never fails")
+ .expect("the newly-created channel exists");
+ assert_eq!(response, channel);
let mut events = app
.events()
.subscribe(None)
.await
.expect("subscribing never fails")
- .filter(fixtures::filter::created());
+ .filter_map(fixtures::channel::events)
+ .filter_map(fixtures::channel::created)
+ .filter(|event| future::ready(event.channel == response));
let event = events
.next()
@@ -49,11 +56,7 @@ async fn new_channel() {
.await
.expect("creation event published");
- assert!(matches!(
- event,
- event::Event::Channel(channel::Event::Created(event))
- if event.channel == response_channel
- ));
+ assert_eq!(event.channel, response);
}
#[tokio::test]
@@ -81,3 +84,72 @@ async fn duplicate_name() {
app::CreateError::DuplicateName(name) if channel.name == name
));
}
+
+#[tokio::test]
+async fn name_reusable_after_delete() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let creator = fixtures::login::create(&app, &fixtures::now()).await;
+ let name = fixtures::channel::propose();
+
+ // Call the endpoint (first time)
+
+ let request = post::Request { name: name.clone() };
+ let Json(response) = post::handler(
+ State(app.clone()),
+ creator.clone(),
+ fixtures::now(),
+ Json(request),
+ )
+ .await
+ .expect("new channel in an empty app");
+
+ // Delete the channel
+
+ app.channels()
+ .delete(&response.id, &fixtures::now())
+ .await
+ .expect("deleting a newly-created channel succeeds");
+
+ // Call the endpoint (second time)
+
+ let request = post::Request { name: name.clone() };
+ let Json(response) = post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
+ .await
+ .expect("new channel in an empty app");
+
+ // Verify the structure of the response
+
+ assert_eq!(name, response.name);
+
+ // Verify the semantics
+
+ let snapshot = app.boot().snapshot().await.expect("boot always succeeds");
+ assert!(snapshot.channels.iter().any(|channel| channel == &response));
+
+ let channel = app
+ .channels()
+ .get(&response.id)
+ .await
+ .expect("searching for channels by ID never fails")
+ .expect("the newly-created channel exists");
+ assert_eq!(response, channel);
+
+ let mut events = app
+ .events()
+ .subscribe(None)
+ .await
+ .expect("subscribing never fails")
+ .filter_map(fixtures::channel::events)
+ .filter_map(fixtures::channel::created)
+ .filter(|event| future::ready(event.channel == response));
+
+ let event = events
+ .next()
+ .immediately()
+ .await
+ .expect("creation event published");
+
+ assert_eq!(event.channel, response);
+}
diff --git a/src/channel/snapshot.rs b/src/channel/snapshot.rs
index d4d1d27..2b7d89a 100644
--- a/src/channel/snapshot.rs
+++ b/src/channel/snapshot.rs
@@ -2,11 +2,14 @@ use super::{
event::{Created, Event},
Id,
};
+use crate::clock::DateTime;
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct Channel {
pub id: Id,
pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub deleted_at: Option<DateTime>,
}
impl Channel {
diff --git a/src/event/repo.rs b/src/event/repo.rs
index 40d6a53..56beeea 100644
--- a/src/event/repo.rs
+++ b/src/event/repo.rs
@@ -29,10 +29,7 @@ impl<'c> Sequences<'c> {
.fetch_one(&mut *self.0)
.await?;
- Ok(Instant {
- at: *at,
- sequence: next,
- })
+ Ok(Instant::new(*at, next))
}
pub async fn current(&mut self) -> Result<Sequence, sqlx::Error> {
diff --git a/src/event/routes/test.rs b/src/event/routes/test.rs
index 249f5c2..e6a8b9d 100644
--- a/src/event/routes/test.rs
+++ b/src/event/routes/test.rs
@@ -31,7 +31,7 @@ async fn includes_historical_message() {
// Verify the structure of the response.
let event = events
- .filter(fixtures::filter::messages())
+ .filter_map(fixtures::message::events)
.next()
.immediately()
.await
@@ -62,7 +62,7 @@ async fn includes_live_message() {
let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await;
let event = events
- .filter(fixtures::filter::messages())
+ .filter_map(fixtures::message::events)
.next()
.immediately()
.await
@@ -104,7 +104,7 @@ async fn includes_multiple_channels() {
// Verify the structure of the response.
let events = events
- .filter(fixtures::filter::messages())
+ .filter_map(fixtures::message::events)
.take(messages.len())
.collect::<Vec<_>>()
.immediately()
@@ -141,13 +141,15 @@ async fn sequential_messages() {
// Verify the structure of the response.
- let mut events = events.filter(|event| {
- future::ready(
- messages
- .iter()
- .any(|message| fixtures::event::message_sent(event, message)),
- )
- });
+ let mut events = events
+ .filter_map(fixtures::message::events)
+ .filter(|event| {
+ future::ready(
+ messages
+ .iter()
+ .any(|message| fixtures::event::message_sent(event, message)),
+ )
+ });
// Verify delivery in order
for message in &messages {
@@ -193,7 +195,7 @@ async fn resumes_from() {
.expect("subscribe never fails");
let event = events
- .filter(fixtures::filter::messages())
+ .filter_map(fixtures::message::events)
.next()
.immediately()
.await
@@ -217,6 +219,7 @@ async fn resumes_from() {
// Verify the structure of the response.
let events = resumed
+ .filter_map(fixtures::message::events)
.take(later_messages.len())
.collect::<Vec<_>>()
.immediately()
@@ -275,7 +278,7 @@ async fn serial_resume() {
.expect("subscribe never fails");
let events = events
- .filter(fixtures::filter::messages())
+ .filter_map(fixtures::message::events)
.take(initial_messages.len())
.collect::<Vec<_>>()
.immediately()
@@ -313,7 +316,7 @@ async fn serial_resume() {
.expect("subscribe never fails");
let events = events
- .filter(fixtures::filter::messages())
+ .filter_map(fixtures::message::events)
.take(resume_messages.len())
.collect::<Vec<_>>()
.immediately()
@@ -351,7 +354,7 @@ async fn serial_resume() {
.expect("subscribe never fails");
let events = events
- .filter(fixtures::filter::messages())
+ .filter_map(fixtures::message::events)
.take(final_messages.len())
.collect::<Vec<_>>()
.immediately()
@@ -401,6 +404,7 @@ async fn terminates_on_token_expiry() {
];
assert!(events
+ .filter_map(fixtures::message::events)
.filter(|event| future::ready(
messages
.iter()
@@ -452,6 +456,7 @@ async fn terminates_on_logout() {
];
assert!(events
+ .filter_map(fixtures::message::events)
.filter(|event| future::ready(
messages
.iter()
diff --git a/src/event/sequence.rs b/src/event/sequence.rs
index bf6d5b8..9bc399b 100644
--- a/src/event/sequence.rs
+++ b/src/event/sequence.rs
@@ -10,6 +10,17 @@ pub struct Instant {
pub sequence: Sequence,
}
+impl Instant {
+ pub fn new(at: DateTime, sequence: Sequence) -> Self {
+ Self { at, sequence }
+ }
+
+ pub fn optional(at: Option<DateTime>, sequence: Option<Sequence>) -> Option<Self> {
+ at.zip(sequence)
+ .map(|(at, sequence)| Self::new(at, sequence))
+ }
+}
+
impl From<Instant> for Sequence {
fn from(instant: Instant) -> Self {
instant.sequence
diff --git a/src/expire.rs b/src/expire.rs
index eaedc44..1427a8d 100644
--- a/src/expire.rs
+++ b/src/expire.rs
@@ -16,6 +16,8 @@ pub async fn middleware(
app.tokens().expire(&expired_at).await?;
app.invites().expire(&expired_at).await?;
app.messages().expire(&expired_at).await?;
+ app.messages().purge(&expired_at).await?;
app.channels().expire(&expired_at).await?;
+ app.channels().purge(&expired_at).await?;
Ok(next.run(req).await)
}
diff --git a/src/login/repo.rs b/src/login/repo.rs
index 6d6510c..7d0fcb1 100644
--- a/src/login/repo.rs
+++ b/src/login/repo.rs
@@ -49,10 +49,7 @@ impl<'c> Logins<'c> {
id: row.id,
name: row.name,
},
- created: Instant {
- at: row.created_at,
- sequence: row.created_sequence,
- },
+ created: Instant::new(row.created_at, row.created_sequence),
})
.fetch_one(&mut *self.0)
.await?;
@@ -79,10 +76,7 @@ impl<'c> Logins<'c> {
id: row.id,
name: row.name,
},
- created: Instant {
- at: row.created_at,
- sequence: row.created_sequence,
- },
+ created: Instant::new(row.created_at, row.created_sequence),
})
.fetch_all(&mut *self.0)
.await?;
@@ -107,10 +101,7 @@ impl<'c> Logins<'c> {
id: row.id,
name: row.name,
},
- created: Instant {
- at: row.created_at,
- sequence: row.created_sequence,
- },
+ created: Instant::new(row.created_at, row.created_sequence),
})
.fetch_all(&mut *self.0)
.await?;
diff --git a/src/message/app.rs b/src/message/app.rs
index c1bcde6..4e50513 100644
--- a/src/message/app.rs
+++ b/src/message/app.rs
@@ -46,8 +46,17 @@ impl<'a> Messages<'a> {
pub async fn delete(&self, message: &Id, deleted_at: &DateTime) -> Result<(), DeleteError> {
let mut tx = self.db.begin().await?;
+ let message = tx
+ .messages()
+ .by_id(message)
+ .await
+ .not_found(|| DeleteError::NotFound(message.clone()))?;
+ message
+ .as_snapshot()
+ .ok_or_else(|| DeleteError::Deleted(message.id().clone()))?;
+
let deleted = tx.sequence().next(deleted_at).await?;
- let message = tx.messages().delete(message, &deleted).await?;
+ let message = tx.messages().delete(&message, &deleted).await?;
tx.commit().await?;
self.events.broadcast(
@@ -91,6 +100,17 @@ impl<'a> Messages<'a> {
Ok(())
}
+
+ pub async fn purge(&self, relative_to: &DateTime) -> Result<(), sqlx::Error> {
+ // Somewhat arbitrarily, purge after 6 hours.
+ let purge_at = relative_to.to_owned() - TimeDelta::hours(6);
+
+ let mut tx = self.db.begin().await?;
+ tx.messages().purge(&purge_at).await?;
+ tx.commit().await?;
+
+ Ok(())
+ }
}
#[derive(Debug, thiserror::Error)]
@@ -107,6 +127,8 @@ pub enum DeleteError {
ChannelNotFound(channel::Id),
#[error("message {0} not found")]
NotFound(Id),
+ #[error("message {0} deleted")]
+ Deleted(Id),
#[error(transparent)]
Database(#[from] sqlx::Error),
}
diff --git a/src/message/history.rs b/src/message/history.rs
index 09e69b7..0424d0d 100644
--- a/src/message/history.rs
+++ b/src/message/history.rs
@@ -30,6 +30,11 @@ impl History {
.filter(Sequence::up_to(resume_point.into()))
.collect()
}
+
+ // Snapshot of this message as of all events recorded in this history.
+ pub fn as_snapshot(&self) -> Option<Message> {
+ self.events().collect()
+ }
}
// Events interface
diff --git a/src/message/repo.rs b/src/message/repo.rs
index 71c6d10..85a69fc 100644
--- a/src/message/repo.rs
+++ b/src/message/repo.rs
@@ -53,14 +53,12 @@ impl<'c> Messages<'c> {
)
.map(|row| History {
message: Message {
- sent: Instant {
- at: row.sent_at,
- sequence: row.sent_sequence,
- },
+ sent: Instant::new(row.sent_at, row.sent_sequence),
channel: row.channel,
sender: row.sender,
id: row.id,
- body: row.body,
+ body: row.body.unwrap_or_default(),
+ deleted_at: None,
},
deleted: None,
})
@@ -70,41 +68,37 @@ impl<'c> Messages<'c> {
Ok(message)
}
- pub async fn in_channel(
- &mut self,
- channel: &channel::History,
- resume_at: ResumePoint,
- ) -> Result<Vec<History>, sqlx::Error> {
+ pub async fn live(&mut self, channel: &channel::History) -> Result<Vec<History>, sqlx::Error> {
let channel_id = channel.id();
let messages = sqlx::query!(
r#"
select
- channel as "channel: channel::Id",
- sender as "sender: login::Id",
+ message.channel as "channel: channel::Id",
+ message.sender as "sender: login::Id",
id as "id: Id",
- body,
- sent_at as "sent_at: DateTime",
- sent_sequence as "sent_sequence: Sequence"
+ message.body,
+ message.sent_at as "sent_at: DateTime",
+ message.sent_sequence as "sent_sequence: Sequence",
+ deleted.deleted_at as "deleted_at: DateTime",
+ deleted.deleted_sequence as "deleted_sequence: Sequence"
from message
- where channel = $1
- and coalesce(sent_sequence <= $2, true)
- order by sent_sequence
+ left join message_deleted as deleted
+ using (id)
+ where message.channel = $1
+ and deleted.id is null
"#,
channel_id,
- resume_at,
)
.map(|row| History {
message: Message {
- sent: Instant {
- at: row.sent_at,
- sequence: row.sent_sequence,
- },
+ sent: Instant::new(row.sent_at, row.sent_sequence),
channel: row.channel,
sender: row.sender,
id: row.id,
- body: row.body,
+ body: row.body.unwrap_or_default(),
+ deleted_at: row.deleted_at,
},
- deleted: None,
+ deleted: Instant::optional(row.deleted_at, row.deleted_sequence),
})
.fetch_all(&mut *self.0)
.await?;
@@ -116,30 +110,32 @@ impl<'c> Messages<'c> {
let messages = sqlx::query!(
r#"
select
- channel as "channel: channel::Id",
- sender as "sender: login::Id",
+ message.channel as "channel: channel::Id",
+ message.sender as "sender: login::Id",
id as "id: Id",
- body,
- sent_at as "sent_at: DateTime",
- sent_sequence as "sent_sequence: Sequence"
+ message.body,
+ message.sent_at as "sent_at: DateTime",
+ message.sent_sequence as "sent_sequence: Sequence",
+ deleted.deleted_at as "deleted_at: DateTime",
+ deleted.deleted_sequence as "deleted_sequence: Sequence"
from message
- where coalesce(sent_sequence <= $2, true)
- order by sent_sequence
+ left join message_deleted as deleted
+ using (id)
+ where coalesce(message.sent_sequence <= $2, true)
+ order by message.sent_sequence
"#,
resume_at,
)
.map(|row| History {
message: Message {
- sent: Instant {
- at: row.sent_at,
- sequence: row.sent_sequence,
- },
+ sent: Instant::new(row.sent_at, row.sent_sequence),
channel: row.channel,
sender: row.sender,
id: row.id,
- body: row.body,
+ body: row.body.unwrap_or_default(),
+ deleted_at: row.deleted_at,
},
- deleted: None,
+ deleted: Instant::optional(row.deleted_at, row.deleted_sequence),
})
.fetch_all(&mut *self.0)
.await?;
@@ -147,33 +143,35 @@ impl<'c> Messages<'c> {
Ok(messages)
}
- async fn by_id(&mut self, message: &Id) -> Result<History, sqlx::Error> {
+ pub async fn by_id(&mut self, message: &Id) -> Result<History, sqlx::Error> {
let message = sqlx::query!(
r#"
select
- channel as "channel: channel::Id",
- sender as "sender: login::Id",
+ message.channel as "channel: channel::Id",
+ message.sender as "sender: login::Id",
id as "id: Id",
- body,
- sent_at as "sent_at: DateTime",
- sent_sequence as "sent_sequence: Sequence"
+ message.body,
+ message.sent_at as "sent_at: DateTime",
+ message.sent_sequence as "sent_sequence: Sequence",
+ deleted.deleted_at as "deleted_at?: DateTime",
+ deleted.deleted_sequence as "deleted_sequence?: Sequence"
from message
+ left join message_deleted as deleted
+ using (id)
where id = $1
"#,
message,
)
.map(|row| History {
message: Message {
- sent: Instant {
- at: row.sent_at,
- sequence: row.sent_sequence,
- },
+ sent: Instant::new(row.sent_at, row.sent_sequence),
channel: row.channel,
sender: row.sender,
id: row.id,
- body: row.body,
+ body: row.body.unwrap_or_default(),
+ deleted_at: row.deleted_at,
},
- deleted: None,
+ deleted: Instant::optional(row.deleted_at, row.deleted_sequence),
})
.fetch_one(&mut *self.0)
.await?;
@@ -183,39 +181,103 @@ impl<'c> Messages<'c> {
pub async fn delete(
&mut self,
- message: &Id,
+ message: &History,
deleted: &Instant,
) -> Result<History, sqlx::Error> {
- let history = self.by_id(message).await?;
+ let id = message.id();
sqlx::query_scalar!(
r#"
- delete from message
- where
- id = $1
- returning 1 as "deleted: i64"
+ insert into message_deleted (id, deleted_at, deleted_sequence)
+ values ($1, $2, $3)
+ returning 1 as "deleted: bool"
"#,
- history.message.id,
+ id,
+ deleted.at,
+ deleted.sequence,
)
.fetch_one(&mut *self.0)
.await?;
- Ok(History {
- deleted: Some(*deleted),
- ..history
- })
+ // Small social responsibility hack here: when a message is deleted, its body is
+ // retconned to have been the empty string. Someone reading the event stream
+ // afterwards, or looking at messages in the channel, cannot retrieve the
+ // "deleted" message by ignoring the deletion event.
+ sqlx::query_scalar!(
+ r#"
+ update message
+ set body = ""
+ where id = $1
+ returning 1 as "blanked: bool"
+ "#,
+ id,
+ )
+ .fetch_one(&mut *self.0)
+ .await?;
+
+ let message = self.by_id(id).await?;
+
+ Ok(message)
}
- pub async fn expired(&mut self, expire_at: &DateTime) -> Result<Vec<Id>, sqlx::Error> {
+ pub async fn purge(&mut self, purge_at: &DateTime) -> Result<(), sqlx::Error> {
let messages = sqlx::query_scalar!(
r#"
+ delete from message_deleted
+ where deleted_at < $1
+ returning id as "id: Id"
+ "#,
+ purge_at,
+ )
+ .fetch_all(&mut *self.0)
+ .await?;
+
+ for message in messages {
+ sqlx::query!(
+ r#"
+ delete from message
+ where id = $1
+ "#,
+ message,
+ )
+ .execute(&mut *self.0)
+ .await?;
+ }
+
+ Ok(())
+ }
+
+ pub async fn expired(&mut self, expire_at: &DateTime) -> Result<Vec<History>, sqlx::Error> {
+ let messages = sqlx::query!(
+ r#"
select
- id as "message: Id"
+ id as "id: Id",
+ message.channel as "channel: channel::Id",
+ message.sender as "sender: login::Id",
+ message.sent_at as "sent_at: DateTime",
+ message.sent_sequence as "sent_sequence: Sequence",
+ message.body,
+ deleted.deleted_at as "deleted_at?: DateTime",
+ deleted.deleted_sequence as "deleted_sequence?: Sequence"
from message
- where sent_at < $1
+ left join message_deleted as deleted
+ using (id)
+ where message.sent_at < $1
+ and deleted.id is null
"#,
expire_at,
)
+ .map(|row| History {
+ message: Message {
+ sent: Instant::new(row.sent_at, row.sent_sequence),
+ id: row.id,
+ channel: row.channel,
+ sender: row.sender,
+ body: row.body.unwrap_or_default(),
+ deleted_at: row.deleted_at,
+ },
+ deleted: Instant::optional(row.deleted_at, row.deleted_sequence),
+ })
.fetch_all(&mut *self.0)
.await?;
@@ -226,29 +288,31 @@ impl<'c> Messages<'c> {
let messages = sqlx::query!(
r#"
select
- channel as "channel: channel::Id",
- sender as "sender: login::Id",
id as "id: Id",
- body,
- sent_at as "sent_at: DateTime",
- sent_sequence as "sent_sequence: Sequence"
+ message.channel as "channel: channel::Id",
+ message.sender as "sender: login::Id",
+ message.sent_at as "sent_at: DateTime",
+ message.sent_sequence as "sent_sequence: Sequence",
+ message.body,
+ deleted.deleted_at as "deleted_at: DateTime",
+ deleted.deleted_sequence as "deleted_sequence: Sequence"
from message
+ left join message_deleted as deleted
+ using (id)
where coalesce(message.sent_sequence > $1, true)
"#,
resume_at,
)
.map(|row| History {
message: Message {
- sent: Instant {
- at: row.sent_at,
- sequence: row.sent_sequence,
- },
+ sent: Instant::new(row.sent_at, row.sent_sequence),
channel: row.channel,
sender: row.sender,
id: row.id,
- body: row.body,
+ body: row.body.unwrap_or_default(),
+ deleted_at: row.deleted_at,
},
- deleted: None,
+ deleted: Instant::optional(row.deleted_at, row.deleted_sequence),
})
.fetch_all(&mut *self.0)
.await?;
diff --git a/src/message/routes/message.rs b/src/message/routes/message.rs
index 059b8c1..fbef35a 100644
--- a/src/message/routes/message.rs
+++ b/src/message/routes/message.rs
@@ -33,9 +33,9 @@ pub mod delete {
let Self(error) = self;
#[allow(clippy::match_wildcard_for_single_variants)]
match error {
- DeleteError::ChannelNotFound(_) | DeleteError::NotFound(_) => {
- NotFound(error).into_response()
- }
+ DeleteError::ChannelNotFound(_)
+ | DeleteError::NotFound(_)
+ | DeleteError::Deleted(_) => NotFound(error).into_response(),
other => Internal::from(other).into_response(),
}
}
diff --git a/src/message/snapshot.rs b/src/message/snapshot.rs
index 0eb37bb..7300918 100644
--- a/src/message/snapshot.rs
+++ b/src/message/snapshot.rs
@@ -2,7 +2,7 @@ use super::{
event::{Event, Sent},
Id,
};
-use crate::{channel, event::Instant, login};
+use crate::{channel, clock::DateTime, event::Instant, login};
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct Message {
@@ -12,6 +12,8 @@ pub struct Message {
pub sender: login::Id,
pub id: Id,
pub body: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub deleted_at: Option<DateTime>,
}
impl Message {
diff --git a/src/test/fixtures/channel.rs b/src/test/fixtures/channel.rs
index b678717..a1dda61 100644
--- a/src/test/fixtures/channel.rs
+++ b/src/test/fixtures/channel.rs
@@ -1,10 +1,17 @@
+use std::future;
+
use faker_rand::{
en_us::{addresses::CityName, names::FullName},
faker_impl_from_templates,
};
use rand;
-use crate::{app::App, channel::Channel, clock::RequestedAt};
+use crate::{
+ app::App,
+ channel::{self, Channel},
+ clock::RequestedAt,
+ event::Event,
+};
pub async fn create(app: &App, created_at: &RequestedAt) -> Channel {
let name = propose();
@@ -22,3 +29,17 @@ struct Name(String);
faker_impl_from_templates! {
Name; "{} {}", CityName, FullName;
}
+
+pub fn events(event: Event) -> future::Ready<Option<channel::Event>> {
+ future::ready(match event {
+ Event::Channel(channel) => Some(channel),
+ _ => None,
+ })
+}
+
+pub fn created(event: channel::Event) -> future::Ready<Option<channel::event::Created>> {
+ future::ready(match event {
+ channel::Event::Created(event) => Some(event),
+ channel::Event::Deleted(_) => None,
+ })
+}
diff --git a/src/test/fixtures/event.rs b/src/test/fixtures/event.rs
index 7fe2bf3..fa4fbc0 100644
--- a/src/test/fixtures/event.rs
+++ b/src/test/fixtures/event.rs
@@ -1,11 +1,8 @@
-use crate::{
- event::Event,
- message::{Event::Sent, Message},
-};
+use crate::message::{Event, Message};
pub fn message_sent(event: &Event, message: &Message) -> bool {
matches!(
&event,
- Event::Message(Sent(event)) if message == &event.into()
+ Event::Sent(event) if message == &event.into()
)
}
diff --git a/src/test/fixtures/filter.rs b/src/test/fixtures/filter.rs
deleted file mode 100644
index 84d27b0..0000000
--- a/src/test/fixtures/filter.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use futures::future;
-
-use crate::{channel::Event::Created, event::Event, message::Event::Sent};
-
-pub fn messages() -> impl FnMut(&Event) -> future::Ready<bool> {
- |event| future::ready(matches!(event, Event::Message(Sent(_))))
-}
-
-pub fn created() -> impl FnMut(&Event) -> future::Ready<bool> {
- |event| future::ready(matches!(event, Event::Channel(Created(_))))
-}
diff --git a/src/test/fixtures/message.rs b/src/test/fixtures/message.rs
index 381b10b..eb00e7c 100644
--- a/src/test/fixtures/message.rs
+++ b/src/test/fixtures/message.rs
@@ -1,6 +1,15 @@
+use std::future;
+
use faker_rand::lorem::Paragraphs;
-use crate::{app::App, channel::Channel, clock::RequestedAt, login::Login, message::Message};
+use crate::{
+ app::App,
+ channel::Channel,
+ clock::RequestedAt,
+ event::Event,
+ login::Login,
+ message::{self, Message},
+};
pub async fn send(app: &App, channel: &Channel, login: &Login, sent_at: &RequestedAt) -> Message {
let body = propose();
@@ -14,3 +23,10 @@ pub async fn send(app: &App, channel: &Channel, login: &Login, sent_at: &Request
pub fn propose() -> String {
rand::random::<Paragraphs>().to_string()
}
+
+pub fn events(event: Event) -> future::Ready<Option<message::Event>> {
+ future::ready(match event {
+ Event::Message(event) => Some(event),
+ _ => None,
+ })
+}
diff --git a/src/test/fixtures/mod.rs b/src/test/fixtures/mod.rs
index 41f7e13..9658831 100644
--- a/src/test/fixtures/mod.rs
+++ b/src/test/fixtures/mod.rs
@@ -4,7 +4,6 @@ use crate::{app::App, clock::RequestedAt, db};
pub mod channel;
pub mod event;
-pub mod filter;
pub mod future;
pub mod identity;
pub mod login;
diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs
index 9aee81f..88d0878 100644
--- a/src/token/repo/auth.rs
+++ b/src/token/repo/auth.rs
@@ -40,10 +40,7 @@ impl<'t> Auth<'t> {
id: row.id,
name: row.name,
},
- created: Instant {
- at: row.created_at,
- sequence: row.created_sequence,
- },
+ created: Instant::new(row.created_at, row.created_sequence),
},
row.password_hash,
)