Skip to content

Commit

Permalink
Merge pull request #4871 from systeminit/victor/eng-2803-implement-th…
Browse files Browse the repository at this point in the history
…e-views-cru

feat: Create update and list endpoints for views
  • Loading branch information
vbustamante authored Oct 25, 2024
2 parents 9a5208c + b1d59e9 commit d4000ef
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 5 deletions.
79 changes: 77 additions & 2 deletions lib/dal/src/diagram/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ use crate::{
id, implement_add_edge_to, EdgeWeightKindDiscriminants, Timestamp, WorkspaceSnapshotError,
};
use crate::{DalContext, EdgeWeightKind};
use jwt_simple::prelude::{Deserialize, Serialize};
use chrono::Utc;
use petgraph::Outgoing;
use serde::{Deserialize, Serialize};
use si_events::ulid::Ulid;
use si_events::ContentHash;
use std::sync::Arc;
Expand Down Expand Up @@ -47,6 +48,20 @@ impl View {
self.name.as_ref()
}

pub fn id(&self) -> ViewId {
self.id
}

pub fn timestamp(&self) -> &Timestamp {
&self.timestamp
}

pub async fn is_default(&self, ctx: &DalContext) -> DiagramResult<bool> {
let default_id = Self::get_id_for_default(ctx).await?;

Ok(default_id == self.id)
}

fn assemble(node_weight: ViewNodeWeight, content: ViewContent) -> Self {
let content = content.extract();

Expand Down Expand Up @@ -96,6 +111,63 @@ impl View {
Ok(Self::assemble(node_weight, content))
}

pub async fn find_by_name(ctx: &DalContext, name: &str) -> DiagramResult<Option<Self>> {
for view_node_weight in Self::list_node_weights(ctx).await? {
let content = Self::try_get_content(ctx, &view_node_weight.content_hash())
.await?
.ok_or(WorkspaceSnapshotError::MissingContentFromStore(
view_node_weight.id(),
))?;

let view = Self::assemble(view_node_weight, content);

if view.name == name {
return Ok(Some(view));
}
}

Ok(None)
}

async fn list_node_weights(ctx: &DalContext) -> DiagramResult<Vec<ViewNodeWeight>> {
let snap = ctx.workspace_snapshot()?;

let category_node = snap
.get_category_node(None, CategoryNodeKind::View)
.await?
.ok_or(DiagramError::ViewCategoryNotFound)?;

let mut views = vec![];
for view_idx in snap
.outgoing_targets_for_edge_weight_kind(category_node, EdgeWeightKindDiscriminants::Use)
.await?
{
let view_node_weight = snap
.get_node_weight(view_idx)
.await?
.get_view_node_weight()?;

views.push(view_node_weight);
}

Ok(views)
}

pub async fn list(ctx: &DalContext) -> DiagramResult<Vec<Self>> {
let mut views = vec![];
for view_node_weight in Self::list_node_weights(ctx).await? {
let content = Self::try_get_content(ctx, &view_node_weight.content_hash())
.await?
.ok_or(WorkspaceSnapshotError::MissingContentFromStore(
view_node_weight.id(),
))?;

views.push(Self::assemble(view_node_weight, content));
}

Ok(views)
}

pub async fn get_id_for_default(ctx: &DalContext) -> DiagramResult<ViewId> {
let snap = ctx.workspace_snapshot()?;

Expand Down Expand Up @@ -187,7 +259,10 @@ impl View {
.write(
Arc::new(
ViewContent::V1(ViewContentV1 {
timestamp: self.timestamp,
timestamp: Timestamp {
created_at: self.timestamp.created_at,
updated_at: Utc::now(),
},
name: name.as_ref().to_owned(),
})
.into(),
Expand Down
5 changes: 2 additions & 3 deletions lib/sdf-server/src/service/diagram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ use telemetry::prelude::*;
use thiserror::Error;
use tokio::task::JoinError;

use crate::AppState;

use super::ApiError;
use crate::AppState;

pub mod create_component;
pub mod create_connection;
Expand Down Expand Up @@ -117,7 +116,7 @@ pub enum DiagramError {
#[error("No installable module found for schema id {0}")]
UninstalledSchemaNotFound(SchemaId),
#[error(transparent)]
WorkspaceSnaphot(#[from] WorkspaceSnapshotError),
WorkspaceSnasphot(#[from] WorkspaceSnapshotError),
#[error("ws event error: {0}")]
WsEvent(#[from] WsEventError),
}
Expand Down
2 changes: 2 additions & 0 deletions lib/sdf-server/src/service/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod func;
pub mod management;
pub mod module;
pub mod variant;
pub mod view;

const PREFIX: &str = "/workspaces/:workspace_id/change-sets/:change_set_id";

Expand All @@ -19,4 +20,5 @@ pub fn routes(state: AppState) -> Router<AppState> {
.nest(&format!("{PREFIX}/modules"), module::v2_routes())
.nest(&format!("{PREFIX}/schema-variants"), variant::v2_routes())
.nest(&format!("{PREFIX}/management"), management::v2_routes())
.nest(&format!("{PREFIX}/views"), view::v2_routes())
}
72 changes: 72 additions & 0 deletions lib/sdf-server/src/service/v2/view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use crate::app_state::AppState;
use crate::service::ApiError;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post, put};
use axum::Router;
use dal::diagram::view::{View, ViewId};
use dal::{ChangeSetError, DalContext, Timestamp, TransactionsError};
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub mod create_view;
pub mod list_views;
pub mod update_view;

#[remain::sorted]
#[derive(Debug, Error)]
pub enum ViewError {
#[error("changeset error: {0}")]
ChangeSet(#[from] ChangeSetError),
#[error("dal diagram error: {0}")]
DalDiagram(#[from] dal::diagram::DiagramError),
#[error("there is already a view called {0}")]
NameAlreadyInUse(String),
#[error("transactions error: {0}")]
Transactions(#[from] TransactionsError),
}

pub type ViewResult<T> = Result<T, ViewError>;

impl IntoResponse for ViewError {
fn into_response(self) -> Response {
let (status_code, error_message) = match self {
ViewError::NameAlreadyInUse(_) => (StatusCode::UNPROCESSABLE_ENTITY, self.to_string()),

_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
};

ApiError::new(status_code, error_message).into_response()
}
}

/// Frontend representation for a [View](View).
/// Yeah, it's a silly name, but all the other frontend representation structs are *View,
/// so we either keep it or change everything.
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct ViewView {
id: ViewId,
name: String,
is_default: bool,
#[serde(flatten)]
timestamp: Timestamp,
}

impl ViewView {
pub async fn from_view(ctx: &DalContext, view: View) -> ViewResult<Self> {
Ok(ViewView {
id: view.id(),
name: view.name().to_owned(),
is_default: view.is_default(ctx).await?,
timestamp: view.timestamp().to_owned(),
})
}
}

pub fn v2_routes() -> Router<AppState> {
Router::new()
// Func Stuff
.route("/", get(list_views::list_views))
.route("/", post(create_view::create_view))
.route("/:view_id", put(update_view::update_view))
}
58 changes: 58 additions & 0 deletions lib/sdf-server/src/service/v2/view/create_view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::extract::{AccessBuilder, HandlerContext, PosthogClient};
use crate::service::force_change_set_response::ForceChangeSetResponse;
use crate::service::v2::view::{ViewError, ViewResult, ViewView};
use crate::tracking::track;
use axum::extract::{Host, OriginalUri, Path};
use axum::Json;
use dal::diagram::view::View;
use dal::{ChangeSet, ChangeSetId, WorkspacePk};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Request {
pub name: String,
}

pub async fn create_view(
HandlerContext(builder): HandlerContext,
AccessBuilder(access_builder): AccessBuilder,
PosthogClient(posthog_client): PosthogClient,
OriginalUri(original_uri): OriginalUri,
Host(host_name): Host,
Path((_workspace_pk, change_set_id)): Path<(WorkspacePk, ChangeSetId)>,
Json(Request { name }): Json<Request>,
) -> ViewResult<ForceChangeSetResponse<ViewView>> {
let mut ctx = builder
.build(access_builder.build(change_set_id.into()))
.await?;

if View::find_by_name(&ctx, name.as_str()).await?.is_some() {
return Err(ViewError::NameAlreadyInUse(name));
}

let force_change_set_id = ChangeSet::force_new(&mut ctx).await?;

let view = View::new(&ctx, name).await?;

track(
&posthog_client,
&ctx,
&original_uri,
&host_name,
"create_view",
serde_json::json!({
"how": "/diagram/create_view",
"view_id": view.id(),
"view_name": view.name(),
"change_set_id": ctx.change_set_id(),
}),
);

ctx.commit().await?;

Ok(ForceChangeSetResponse::new(
force_change_set_id,
ViewView::from_view(&ctx, view).await?,
))
}
32 changes: 32 additions & 0 deletions lib/sdf-server/src/service/v2/view/list_views.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use crate::extract::{AccessBuilder, HandlerContext};
use crate::service::v2::view::{ViewResult, ViewView};
use axum::extract::{Json, Path};
use dal::diagram::view::View;
use dal::{ChangeSetId, Visibility, WorkspacePk};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Request {
#[serde(flatten)]
pub visibility: Visibility,
}

pub type Response = Vec<ViewView>;

pub async fn list_views(
HandlerContext(builder): HandlerContext,
AccessBuilder(access_builder): AccessBuilder,
Path((_workspace_pk, change_set_id)): Path<(WorkspacePk, ChangeSetId)>,
) -> ViewResult<Json<Response>> {
let ctx = builder
.build(access_builder.build(change_set_id.into()))
.await?;

let mut views = vec![];
for view in View::list(&ctx).await? {
views.push(ViewView::from_view(&ctx, view).await?);
}

Ok(Json(views))
}
68 changes: 68 additions & 0 deletions lib/sdf-server/src/service/v2/view/update_view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::extract::{AccessBuilder, HandlerContext, PosthogClient};
use crate::service::force_change_set_response::ForceChangeSetResponse;
use crate::service::v2::view::{ViewError, ViewResult};
use crate::tracking::track;
use axum::extract::{Host, OriginalUri, Path};
use axum::Json;
use dal::diagram::view::{View, ViewId};
use dal::{ChangeSet, ChangeSetId, WorkspacePk};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Request {
pub name: String,
}

pub async fn update_view(
HandlerContext(builder): HandlerContext,
AccessBuilder(access_builder): AccessBuilder,
PosthogClient(posthog_client): PosthogClient,
OriginalUri(original_uri): OriginalUri,
Host(host_name): Host,
Path((_workspace_pk, change_set_id, view_id)): Path<(WorkspacePk, ChangeSetId, ViewId)>,
Json(Request { name }): Json<Request>,
) -> ViewResult<ForceChangeSetResponse<()>> {
let mut ctx = builder
.build(access_builder.build(change_set_id.into()))
.await?;

// NOTE(victor) We want to still move the user to a new changeset if they ran an update event,
// just don't change any data if they tried to rename the changeset to the name it already has
let should_update = if let Some(view) = View::find_by_name(&ctx, name.as_str()).await? {
if view.id() == view_id {
false
} else {
return Err(ViewError::NameAlreadyInUse(name));
}
} else {
true
};

let force_change_set_id = ChangeSet::force_new(&mut ctx).await?;

if should_update {
let mut view = View::get_by_id(&ctx, view_id).await?;
let old_view_name = view.name().to_owned();
view.set_name(&ctx, name).await?;

track(
&posthog_client,
&ctx,
&original_uri,
&host_name,
"update_view",
serde_json::json!({
"how": "/diagram/update_view",
"view_id": view.id(),
"view_new_name": view.name(),
"view_old_name": old_view_name,
"change_set_id": ctx.change_set_id(),
}),
);
}

ctx.commit().await?;

Ok(ForceChangeSetResponse::new(force_change_set_id, ()))
}

0 comments on commit d4000ef

Please sign in to comment.