Skip to content

Commit

Permalink
Add backend for querying refs
Browse files Browse the repository at this point in the history
  • Loading branch information
jmoggr committed Feb 17, 2025
1 parent a5187e5 commit 09b0363
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 1 deletion.
7 changes: 7 additions & 0 deletions packages/backend/pkg/src/RefStub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Information to allow the display and referencing of documents without
* loading the documents contents
*/
export type RefStub = { name: string, ref_id: string, };
4 changes: 3 additions & 1 deletion packages/backend/pkg/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { NewPermissions } from "./NewPermissions.ts";
import type { UserSummary } from "./UserSummary.ts";
import type { UsernameStatus } from "./UsernameStatus.ts";
import type { UserProfile } from "./UserProfile.ts";
import type { RefStub } from "./RefStub.ts";

export type { RpcResult } from "./RpcResult.ts";
export type { JsonValue } from "./serde_json/JsonValue.ts";
Expand All @@ -35,5 +36,6 @@ export type { NewPermissions } from "./NewPermissions.ts";
export type { UserSummary } from "./UserSummary.ts";
export type { UsernameStatus } from "./UsernameStatus.ts";
export type { UserProfile } from "./UserProfile.ts";
export type { RefStub } from "./RefStub.ts";

export type QubitServer = { new_ref: Mutation<[content: JsonValue, ], RpcResult<string>>, get_doc: Query<[ref_id: string, ], RpcResult<RefDoc>>, head_snapshot: Query<[ref_id: string, ], RpcResult<JsonValue>>, save_snapshot: Mutation<[data: RefContent, ], RpcResult<null>>, get_permissions: Query<[ref_id: string, ], RpcResult<Permissions>>, set_permissions: Mutation<[ref_id: string, new: NewPermissions, ], RpcResult<null>>, sign_up_or_sign_in: Mutation<[], RpcResult<null>>, user_by_username: Query<[username: string, ], RpcResult<UserSummary | null>>, username_status: Query<[username: string, ], RpcResult<UsernameStatus>>, get_active_user_profile: Query<[], RpcResult<UserProfile>>, set_active_user_profile: Mutation<[user: UserProfile, ], RpcResult<null>> };
export type QubitServer = { new_ref: Mutation<[content: JsonValue, ], RpcResult<string>>, get_doc: Query<[ref_id: string, ], RpcResult<RefDoc>>, head_snapshot: Query<[ref_id: string, ], RpcResult<JsonValue>>, save_snapshot: Mutation<[data: RefContent, ], RpcResult<null>>, get_permissions: Query<[ref_id: string, ], RpcResult<Permissions>>, set_permissions: Mutation<[ref_id: string, new: NewPermissions, ], RpcResult<null>>, sign_up_or_sign_in: Mutation<[], RpcResult<null>>, user_by_username: Query<[username: string, ], RpcResult<UserSummary | null>>, username_status: Query<[username: string, ], RpcResult<UsernameStatus>>, get_active_user_profile: Query<[], RpcResult<UserProfile>>, set_active_user_profile: Mutation<[user: UserProfile, ], RpcResult<null>> };
140 changes: 140 additions & 0 deletions packages/backend/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,143 @@ pub struct RefContent {
pub ref_id: Uuid,
pub content: Value,
}

/// A subset of user relevant information about a ref. Used for showing
/// users information on a variety of refs without having to load whole
/// refs.
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
pub struct RefStub {
pub name: String,
pub ref_id: Uuid,
// TODO: get the types for these fields serializeable
// pub permission_level: PermissionLevel,
// pub created_at
// pub last_updated
}

/// Parameters for filtering a search of refs
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
pub struct RefQueryParams {
pub owner_username_query: Option<String>,
pub ref_name_query: Option<String>,
// TODO: add param for document type
}

/// Gets a vec of user relevant informations about refs that match the query parameters
pub async fn get_ref_stubs(
ctx: AppCtx,
search_params: RefQueryParams,
) -> Result<Vec<RefStub>, AppError> {
let searcher_id = ctx.user.as_ref().map(|user| user.user_id.clone());

// selects the ref id and ref name (from the most recent snapshot of that ref) for all refs that a user (the searcher) has permission to access. Optionally filter the results by the owner of the refs.
let results = sqlx::query!(
r#"
WITH latest_snapshots AS (
SELECT DISTINCT ON (snapshots.for_ref)
refs.id AS ref_id,
snapshots.content->>'name' AS name
FROM snapshots
JOIN refs ON snapshots.for_ref = refs.id
ORDER BY snapshots.for_ref, snapshots.id DESC
)
SELECT
ls.ref_id,
ls.name
FROM latest_snapshots ls
JOIN permissions p_searcher ON ls.ref_id = p_searcher.object
JOIN users owner ON owner.id = (
SELECT p_owner.subject
FROM permissions p_owner
WHERE p_owner.object = ls.ref_id
AND p_owner.level = 'own'
LIMIT 1
)
WHERE p_searcher.subject = $1 -- searcher_id
AND (
owner.username = $2 -- owner_username
OR $2 IS NULL -- include all owners if owner_username is NULL
)
AND p_searcher.level IN ('read', 'write', 'maintain')
AND (
ls.name ILIKE '%' || $3 || '%' -- case-insensitive substring search
OR $3 IS NULL -- include all if name filter is NULL
)
LIMIT 100 -- TODO: pagination;
"#,
searcher_id,
search_params.owner_username_query,
search_params.ref_name_query
)
.fetch_all(&ctx.state.db)
.await?;

// Map SQL query results to Vec<RefStub>
let stubs = results
.into_iter()
.map(|row| RefStub {
ref_id: row.ref_id,
name: row.name.unwrap_or_else(|| "untitled".to_string()),
})
.collect();

Ok(stubs)
}

/// Gets a vec of user relevant informations about refs that are related to a user and match the search parameters
pub async fn get_ref_stubs_related_to_user(
ctx: AppCtx,
search_params: RefQueryParams,
) -> Result<Vec<RefStub>, AppError> {
let searcher_id = ctx.user.as_ref().map(|user| user.user_id.clone());

// for all refs that a user has permissions for, get those that the
// searcher also has access to and filter those by search params. If no
// owner is specified, assume that the owner is the same as the searcher.
let results = sqlx::query!(
r#"
WITH latest_snapshots AS (
SELECT DISTINCT ON (snapshots.for_ref)
refs.id AS ref_id,
snapshots.content->>'name' AS name
FROM snapshots
JOIN refs ON snapshots.for_ref = refs.id
ORDER BY snapshots.for_ref, snapshots.id DESC
)
SELECT
ls.ref_id,
ls.name
FROM latest_snapshots ls
JOIN permissions p_searcher ON ls.ref_id = p_searcher.object
JOIN permissions p_owner ON ls.ref_id = p_owner.object -- Ensure owner has permissions
JOIN users owner ON owner.id = p_owner.subject -- Match permissions entry to owner
WHERE p_searcher.subject = $1 -- searcher_id must have access
AND (
owner.username = COALESCE($2, (SELECT username FROM users WHERE id = $1)) -- Use searcher if owner_username is NULL
)
AND p_owner.level IN ('read', 'write', 'maintain', 'own') -- Owner must have at least one permission
AND p_searcher.level IN ('read', 'write', 'maintain') -- Searcher must have permissions
AND (
ls.name ILIKE '%' || $3 || '%' -- Case-insensitive substring search
OR $3 IS NULL -- Include all if name filter is NULL
)
LIMIT 100 -- TODO: pagination;
"#,
searcher_id,
search_params.owner_username_query,
search_params.ref_name_query
)
.fetch_all(&ctx.state.db)
.await?;

// Map SQL query results to Vec<RefStub>
let stubs = results
.into_iter()
.map(|row| RefStub {
ref_id: row.ref_id,
name: row.name.unwrap_or_else(|| "untitled".to_string()),
})
.collect();

Ok(stubs)
}
18 changes: 18 additions & 0 deletions packages/backend/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub fn router() -> Router<AppState> {
.handler(username_status)
.handler(get_active_user_profile)
.handler(set_active_user_profile)
.handler(get_ref_stubs)
.handler(get_ref_stubs_related_to_user)
}

#[handler(mutation)]
Expand Down Expand Up @@ -74,6 +76,22 @@ enum RefDoc {
},
}

#[handler(query)]
async fn get_ref_stubs(
ctx: AppCtx,
query_params: doc::RefQueryParams,
) -> RpcResult<Vec<doc::RefStub>> {
doc::get_ref_stubs(ctx, query_params).await.into()
}

#[handler(query)]
async fn get_ref_stubs_related_to_user(
ctx: AppCtx,
query_params: doc::RefQueryParams,
) -> RpcResult<Vec<doc::RefStub>> {
doc::get_ref_stubs_related_to_user(ctx, query_params).await.into()
}

#[handler(query)]
async fn head_snapshot(ctx: AppCtx, ref_id: Uuid) -> RpcResult<Value> {
_head_snapshot(ctx, ref_id).await.into()
Expand Down
10 changes: 10 additions & 0 deletions packages/backend/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ pub struct UserProfile {
// pub url: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize, TS)]
pub struct UserProfile2 {
pub username: Option<String>,
#[serde(rename = "displayName")]
pub display_name: Option<String>,
// TODO: More fields, such as:
// pub bio: Option<String>,
// pub url: Option<String>,
}

impl UserProfile {
fn validate(&self) -> Result<(), String> {
if let Some(username) = self.username.as_ref() {
Expand Down

0 comments on commit 09b0363

Please sign in to comment.