Database basics

This commit is contained in:
Riley Apeldoorn 2022-07-21 00:28:28 +02:00
parent 7d392ed3fa
commit 0969bff603
6 changed files with 216 additions and 138 deletions

View file

@ -15,6 +15,21 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils_2": {
"locked": {
"lastModified": 1637014545,
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": { "naersk": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
@ -61,11 +76,47 @@
"type": "indirect" "type": "indirect"
} }
}, },
"nixpkgs_3": {
"locked": {
"lastModified": 1637453606,
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"oxalica": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1655606998,
"narHash": "sha256-6XIQEwmoldCE3lzI54VQxD2tFJoeRsjRMYWkthtGRRw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "4b600525be94c23b44c42441f33460343fafe7a1",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"naersk": "naersk", "naersk": "naersk",
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2",
"oxalica": "oxalica"
} }
} }
}, },

View file

@ -1,32 +1,37 @@
{ {
inputs = { inputs = {
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
oxalica.url = "github:oxalica/rust-overlay";
naersk.url = "github:nix-community/naersk"; naersk.url = "github:nix-community/naersk";
}; };
outputs = { self, nixpkgs, flake-utils, naersk }: outputs = { self, nixpkgs, flake-utils, naersk, oxalica }:
flake-utils.lib.eachDefaultSystem ( flake-utils.lib.eachDefaultSystem (system:
system: let let pkgs = import nixpkgs { inherit system; overlays = [ oxalica.overlay ]; };
pkgs = nixpkgs.legacyPackages."${system}";
naersk-lib = naersk.lib."${system}"; naersk-lib = naersk.lib."${system}";
in in rec {
rec {
# `nix build` # `nix build`
packages."hermit" = naersk-lib.buildPackage { packages."hermit" = naersk-lib.buildPackage {
pname = "hermit"; pname = "hermit";
root = ./.; root = ./.;
}; };
defaultPackage = packages."hermit"; defaultPackage = packages."hermit";
# `nix run` # `nix run`
apps."hermit"= flake-utils.lib.mkApp { apps."hermit"= flake-utils.lib.mkApp {
drv = packages."hermit"; drv = packages."hermit";
}; };
defaultApp = apps."hermit"; defaultApp = apps."hermit";
# `nix develop` # `nix develop`
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ rustc cargo openssl pkgconfig ]; nativeBuildInputs = with pkgs; [
(rust-bin.selectLatestNightlyWith (t: t.default))
openssl
pkgconfig
];
}; };
} }
); );

View file

@ -1,24 +1,25 @@
//! ActivityPub implementation code and related abstractions. //! ActivityPub implementation code and related abstractions.
use futures::prelude::*; use futures::prelude::*;
use serde::Serialize;
use crate::{ Id, Activity, err, Result, Error, sign, ctx::Context }; use crate::{ Id, Activity, err, Result, Error, sign, ctx::Context };
#[derive(Clone)] #[derive(Clone, Serialize)]
pub enum Create { pub enum Create {
Note { object: Note } Note { object: Note }
} }
impl From<Create> for Activity { fn from (a: Create) -> Activity { Activity::Create (a) } } impl From<Create> for Activity { fn from (a: Create) -> Activity { Activity::Create (a) } }
#[derive(Clone)] #[derive(Clone, Serialize)]
pub enum Follow { pub enum Follow {
Actor { object: Actor } Actor { object: Actor }
} }
impl From<Follow> for Activity { fn from (a: Follow) -> Activity { Activity::Follow (a) } } impl From<Follow> for Activity { fn from (a: Follow) -> Activity { Activity::Follow (a) } }
#[derive(Clone)] #[derive(Clone, Serialize)]
pub enum Accept { pub enum Accept {
Follow { object: Follow } Follow { object: Follow }
} }
@ -26,12 +27,12 @@ pub enum Accept {
impl From<Accept> for Activity { fn from (a: Accept) -> Activity { Activity::Accept (a) } } impl From<Accept> for Activity { fn from (a: Accept) -> Activity { Activity::Accept (a) } }
/// An entity that publishes activities. /// An entity that publishes activities.
#[derive(Clone)] #[derive(Clone, Serialize, sqlx::FromRow)]
pub struct Actor { pub struct Actor {
id: Id, id: Id,
} }
#[derive(Clone)] #[derive(Clone, Serialize)]
pub struct Note { pub struct Note {
id: Id, id: Id,
} }

View file

@ -1,13 +1,17 @@
//! Database abstraction layer used by Hermit. //! Database abstraction layer used by Hermit.
use crate::{ Id, Result }; use std::borrow::Borrow;
use std::{marker::PhantomData, pin::Pin};
use crate::{ Id, Result, ap::Actor, err };
use futures::prelude::*; use futures::prelude::*;
use sqlx::{Executor, pool::PoolConnection}; use sqlx::{Executor, pool::PoolConnection, database::HasArguments};
use sqlx::{ FromRow, Either::Right };
/// `const ()` but in Rust /// `const ()` but in Rust
fn void <T> (_: T) -> () { () } fn void <T> (_: T) -> () { () }
type Database = sqlx::Postgres; pub(crate) type Database = sqlx::Postgres;
/// Specifies how to connect to the database. /// Specifies how to connect to the database.
pub struct Config {} pub struct Config {}
@ -28,32 +32,27 @@ impl Client {
todo!() todo!()
} }
/// Fetch the data mapped to the given `key` from the database. pub async fn query <'e, T: 'e> (&self, query: Query<'e, T>) -> Pin<Box<dyn Stream<Item = Result<T>> + Send + 'e>> {
pub async fn get <T> (&self, key: T::Key) -> Result<Option<T>> let Query (q, f) = query;
where let stream = q
T: Get, .fetch_many(&self.pool)
{ .filter_map(async move |r| {
self.with_conn(|c| T::get(key, c)) match r.map_err(err) {
.await Ok (Right (row)) => Some (f(row)),
Err (error) => Some (Err (error)),
_ => None,
}
});
Box::pin (stream)
} }
/// Perfom an insertion on the database. pub async fn get <'q, T: 'q> (&self, query: Query<'q, T>) -> Result<Option<T>> {
pub async fn insert <T> (&mut self, data: T) -> Result<()> self.query(query)
where
T: Insert,
{
self.with_conn(|c| data.set(c))
.await .await
.map(void) .next()
}
/// Delete something from the database.
pub async fn delete <T> (&mut self, key: T::Key) -> Result<()>
where
T: Delete,
{
self.with_conn(|c| T::del(key, c))
.await .await
.transpose()
} }
/// Handles the getting-a-connection logic. /// Handles the getting-a-connection logic.
@ -62,8 +61,6 @@ impl Client {
F: FnOnce (&mut PoolConnection<Database>) -> O, F: FnOnce (&mut PoolConnection<Database>) -> O,
O: Future<Output = Result<T>>, O: Future<Output = Result<T>>,
{ {
use crate::err;
self.pool self.pool
.acquire() .acquire()
.map_err(err) .map_err(err)
@ -75,52 +72,24 @@ impl Client {
} }
pub trait Object: Sized { type Q<'a> = sqlx::query::Query<'a, Database, <Database as HasArguments<'a>>::Arguments>;
type Key: Eq; type Row = <Database as sqlx::Database>::Row;
fn key (&self) -> &Self::Key;
} pub struct Query <'a, T> (Q<'a>, fn (Row) -> Result<T>);
pub trait Insert: Object { /// Generate a query that gets an [`Actor`] by its [`Id`].
type Future: Future<Output = Result<Self::Key>>; pub fn get_actor <'a> (id: &'a Id) -> Query<'a, Actor> {
fn set <'e, E> (self, exec: E) -> Self::Future
where // Prepare a query
E: Executor<'e>; let query = sqlx::query("select * from actors where id = $1")
} .bind(id);
pub trait Delete: Object { // Return an sql query which will result in a series of rows,
type Future: Future<Output = Result<()>>; // and a decoder function that will translate each row to a
fn del <'e, E> (key: Self::Key, exec: E) -> Self::Future where E: Executor<'e>; // value of type `Actor`.
} Query (query, |row: Row| {
let data = Actor::from_row(&row)?;
pub trait Get: Object { Ok (data)
type Future: Future<Output = Result<Option<Self>>>; })
fn get <'e, E> (key: Self::Key, exec: E) -> Self::Future where E: Executor<'e>;
}
pub mod ops {
//! Database operations (queries and updates).
use super::*;
pub struct Following {
pub from: Id,
pub to: Id,
pub id: Id,
}
impl Object for Following {
type Key = Id;
fn key (&self) -> &Self::Key { &self.id }
}
impl Insert for Following {
type Future = future::BoxFuture<'static, Result<Id>>;
fn set <'e, E> (self, exec: E) -> Self::Future
where
E: Executor<'e>
{
todo!()
}
}
} }

View file

@ -1,7 +1,11 @@
#![feature(async_closure)]
//! # The Hermit ActivityPub server //! # The Hermit ActivityPub server
//! //!
//! This library contains the types and trait impls that make up the ActivityPub //! This library contains the types and trait impls that make up the ActivityPub
//! support and database interaction for the Hermit ActivityPub server. //! support and database interaction for the Hermit ActivityPub server.
#![feature(generic_associated_types)]
use serde::Serialize;
// Expose the `Id` type in the crate root // Expose the `Id` type in the crate root
pub use id::Id; pub use id::Id;
@ -14,7 +18,7 @@ pub mod db;
pub mod ap; pub mod ap;
/// The Activity supertype used in abstractions over any kind of activity. /// The Activity supertype used in abstractions over any kind of activity.
#[derive(Clone)] #[derive(Clone, Serialize)]
pub enum Activity { pub enum Activity {
/// Create a post. /// Create a post.
Create (ap::Create), Create (ap::Create),
@ -62,18 +66,19 @@ pub (crate) fn err (e: impl Into<Error>) -> Error { e.into() }
mod id { mod id {
use std::str::FromStr; use std::{str::FromStr, error::Error};
use serde::{ Deserialize, Serialize }; use serde::{ Deserialize, Serialize };
use sqlx::database::{HasArguments, HasValueRef};
use url::ParseError;
use crate::db;
/// An ActivityPub identifier. /// An ActivityPub identifier.
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] #[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Id (reqwest::Url); pub struct Id (reqwest::Url);
impl crate::IntoUrl for Id {
fn into_url (self) -> Option<url::Url> { Some (self.0) }
}
impl FromStr for Id { impl FromStr for Id {
type Err = url::ParseError; type Err = url::ParseError;
@ -82,6 +87,31 @@ mod id {
} }
} }
impl std::fmt::Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.to_string())
}
}
impl sqlx::Type<db::Database> for Id {
fn type_info () -> <db::Database as sqlx::Database>::TypeInfo {
String::type_info()
}
}
impl<'q> sqlx::Encode<'q, db::Database> for Id {
fn encode_by_ref(&self, buf: &mut <db::Database as HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
self.0.to_string().encode_by_ref(buf)
}
}
impl<'r> sqlx::Decode<'r, db::Database> for Id {
fn decode(value: <db::Database as HasValueRef<'r>>::ValueRef) -> Result<Self, sqlx::error::BoxDynError> {
<String as sqlx::Decode<db::Database>>::decode(value)
.map(|s| s.parse().expect("Failed to parse ID as URL"))
.map(Id)
}
}
} }
mod ctx { mod ctx {
@ -89,9 +119,9 @@ mod ctx {
use std::sync::Arc; use std::sync::Arc;
use futures::prelude::*; use futures::prelude::*;
use serde_json::Value; use serde_json::{Value, to_value};
use crate::{ conf::Config, db, Result, sign::Sign, ap, Activity }; use crate::{ conf::Config, db, Result, sign::Sign, ap::{self, Actor}, Activity, Id, err };
/// The context of a thread/task. /// The context of a thread/task.
/// ///
@ -224,6 +254,20 @@ mod ctx {
Ok (value) Ok (value)
}
/// Fetch a value from the database by trying all the ActivityPub
/// records.
async fn db_fetch (&self, id: Id) -> Result<Option<Value>> {
if let Some (data) = self.db.get(db::get_actor(&id)).await? {
return to_value(data)
.map_err(err)
.map(Some)
}
todo!()
} }
/// Attempt to dereference to a [`Create`](ap::Create) activity. /// Attempt to dereference to a [`Create`](ap::Create) activity.

View file

@ -1,11 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use futures::stream;
use hermit::{ Context, Error, db, sign, Activity, }; use hermit::{ Context, Error, db, sign, Activity, };
use hermit::conf::Config; use hermit::conf::Config;
use tokio::sync::{mpsc, broadcast}; use tokio::sync::{mpsc, broadcast};
use task::Executor; use task::Executor;
use tokio_stream::wrappers::{ReceiverStream, BroadcastStream}; use tokio_stream::wrappers::ReceiverStream;
/// Module that contains all the API endpoints and frontend pages /// Module that contains all the API endpoints and frontend pages
/// used by Hermit. /// used by Hermit.
@ -68,6 +68,8 @@ async fn main () {
ctrl_tx, ctrl_tx,
}); });
// Redefine `ctrl_tx`: it is now the transmitter that transmits
// *from* `Ctrl`.
let (ctrl_tx, _rx) = broadcast::channel(256); let (ctrl_tx, _rx) = broadcast::channel(256);
ctx.run (task::Auto { ctx.run (task::Auto {
@ -93,32 +95,28 @@ fn mk_channel <T> (size: usize) -> (mpsc::Sender<T>, ReceiverStream<T>) {
(tx, rx) (tx, rx)
} }
fn err (e: impl Into<Error>) -> Error { e.into() }
mod task { mod task {
//! Async tasks, communicating with each other across threads through generic //! Async tasks, communicating with each other across threads through generic
//! streams and sinks. //! streams and sinks.
use std::pin::Pin; use std::pin::Pin;
use tokio::sync::{mpsc, broadcast}; use tokio::sync::{
broadcast,
mpsc,
};
use futures::prelude::*; use futures::prelude::*;
use serde_json::Value; use serde_json::Value;
use crate::web; use crate::{
use crate::sign::Sign; flow::Flow,
use crate::{flow::Flow, Activity, ctrl::Message, Context}; ctrl::Message,
sign::Sign,
Activity,
Context,
web,
};
impl<S> Executor for Context<S> /// Something that can execute a [`Task`].
where
S: Sign + Send + Sync + 'static
{
fn run (&self, task: impl Task) {
let ctx: Context<S> = self.clone();
tokio::spawn(task.run(ctx));
}
}
/// Something that can execute a task.
pub trait Executor { pub trait Executor {
/// Perform a [`Task`]. /// Perform a [`Task`].
@ -137,6 +135,16 @@ mod task {
where where
S: Sign + Send + Sync + 'static; S: Sign + Send + Sync + 'static;
}
impl<S> Executor for Context<S>
where
S: Sign + Send + Sync + 'static
{
fn run (&self, task: impl Task) {
let ctx: Context<S> = self.clone();
tokio::spawn(task.run(ctx));
}
} }
/// The main web server. /// The main web server.