feat: add workspace foundation
This commit is contained in:
+165
-26
@@ -3,13 +3,15 @@ use std::path::PathBuf;
|
||||
use crank_core::{
|
||||
AuthConfig, AuthKind, AuthProfile, AuthProfileId, ConfigExport, ExportMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, OperationId, OperationStatus, Protocol, SampleId, Samples, Target,
|
||||
Workspace, WorkspaceId, WorkspaceStatus,
|
||||
};
|
||||
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
|
||||
use crank_proto::{ProtoService, services_from_descriptor_set_bytes};
|
||||
use crank_registry::{
|
||||
CreateVersionRequest, OperationSampleMetadata, OperationSummary, OperationVersionRecord,
|
||||
PostgresRegistry, PublishRequest, RegistryOperation, SampleKind, SaveAuthProfileRequest,
|
||||
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, OperationSampleMetadata, OperationSummary,
|
||||
OperationVersionRecord, PostgresRegistry, PublishRequest, RegistryOperation, SampleKind,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
UpdateWorkspaceRequest, WorkspaceRecord,
|
||||
};
|
||||
use crank_runtime::{PreparedRequest, RuntimeError, RuntimeExecutor, RuntimeOperation};
|
||||
use crank_schema::Schema;
|
||||
@@ -76,6 +78,22 @@ pub struct AuthProfilePayload {
|
||||
pub config: AuthConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct WorkspacePayload {
|
||||
pub slug: String,
|
||||
pub display_name: String,
|
||||
#[serde(default)]
|
||||
pub settings: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct UpdateWorkspacePayload {
|
||||
pub slug: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub status: Option<WorkspaceStatus>,
|
||||
pub settings: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GenerateDraftPayload {
|
||||
#[serde(default)]
|
||||
@@ -122,6 +140,7 @@ pub struct YamlOperationDocument {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct CreatedOperationResponse {
|
||||
pub operation_id: String,
|
||||
pub workspace_id: String,
|
||||
pub version: u32,
|
||||
pub status: OperationStatus,
|
||||
}
|
||||
@@ -129,6 +148,7 @@ pub struct CreatedOperationResponse {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PublishResponse {
|
||||
pub operation_id: String,
|
||||
pub workspace_id: String,
|
||||
pub published_version: u32,
|
||||
pub published_at: String,
|
||||
}
|
||||
@@ -136,6 +156,7 @@ pub struct PublishResponse {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ImportResponse {
|
||||
pub operation_id: String,
|
||||
pub workspace_id: String,
|
||||
pub version: u32,
|
||||
pub import_mode: ImportMode,
|
||||
pub warnings: Vec<String>,
|
||||
@@ -172,17 +193,89 @@ impl AdminService {
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_operations(&self) -> Result<Vec<OperationSummary>, ApiError> {
|
||||
Ok(self.registry.list_operations().await?)
|
||||
pub async fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>, ApiError> {
|
||||
Ok(self.registry.list_workspaces().await?)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, payload), fields(workspace_slug = %payload.slug))]
|
||||
pub async fn create_workspace(
|
||||
&self,
|
||||
payload: WorkspacePayload,
|
||||
) -> Result<WorkspaceRecord, ApiError> {
|
||||
let now = now_string()?;
|
||||
let workspace = Workspace {
|
||||
id: WorkspaceId::new(new_prefixed_id("ws")),
|
||||
slug: payload.slug,
|
||||
display_name: payload.display_name,
|
||||
status: WorkspaceStatus::Active,
|
||||
settings: payload.settings,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
self.registry
|
||||
.create_workspace(CreateWorkspaceRequest {
|
||||
workspace: &workspace,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(WorkspaceRecord { workspace })
|
||||
}
|
||||
|
||||
pub async fn get_workspace(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<WorkspaceRecord, ApiError> {
|
||||
self.registry
|
||||
.get_workspace(workspace_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!("workspace {} was not found", workspace_id.as_str()))
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str()))]
|
||||
pub async fn update_workspace(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
payload: UpdateWorkspacePayload,
|
||||
) -> Result<WorkspaceRecord, ApiError> {
|
||||
let existing = self.get_workspace(workspace_id).await?.workspace;
|
||||
let workspace = Workspace {
|
||||
id: existing.id,
|
||||
slug: payload.slug.unwrap_or(existing.slug),
|
||||
display_name: payload.display_name.unwrap_or(existing.display_name),
|
||||
status: payload.status.unwrap_or(existing.status),
|
||||
settings: payload.settings.unwrap_or(existing.settings),
|
||||
created_at: existing.created_at,
|
||||
updated_at: now_string()?,
|
||||
};
|
||||
|
||||
self.registry
|
||||
.update_workspace(UpdateWorkspaceRequest {
|
||||
workspace: &workspace,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(WorkspaceRecord { workspace })
|
||||
}
|
||||
|
||||
pub async fn list_operations(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Vec<OperationSummary>, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
Ok(self.registry.list_operations(workspace_id).await?)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
) -> Result<OperationSummary, ApiError> {
|
||||
self.registry
|
||||
.get_operation_summary(operation_id)
|
||||
.get_operation_summary(workspace_id, operation_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!("operation {} was not found", operation_id.as_str()))
|
||||
@@ -192,11 +285,12 @@ impl AdminService {
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_operation_version(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
version: u32,
|
||||
) -> Result<OperationVersionRecord, ApiError> {
|
||||
self.registry
|
||||
.get_operation_version(operation_id, version)
|
||||
.get_operation_version(workspace_id, operation_id, version)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!(
|
||||
@@ -209,11 +303,17 @@ impl AdminService {
|
||||
#[instrument(skip(self, payload), fields(protocol = ?payload.protocol, operation_name = %payload.name))]
|
||||
pub async fn create_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
payload: OperationPayload,
|
||||
) -> Result<CreatedOperationResponse, ApiError> {
|
||||
self.validate_operation_payload(&payload)?;
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
|
||||
if self.find_operation_by_name(&payload.name).await?.is_some() {
|
||||
if self
|
||||
.find_operation_by_name(workspace_id, &payload.name)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ApiError::conflict(format!(
|
||||
"operation with name {} already exists",
|
||||
payload.name
|
||||
@@ -247,11 +347,14 @@ impl AdminService {
|
||||
published_at: None,
|
||||
};
|
||||
|
||||
self.registry.create_operation(&snapshot, None).await?;
|
||||
self.registry
|
||||
.create_operation(workspace_id, &snapshot, None)
|
||||
.await?;
|
||||
info!(operation_id = %operation_id.as_str(), version = 1, "operation created");
|
||||
|
||||
Ok(CreatedOperationResponse {
|
||||
operation_id: operation_id.as_str().to_owned(),
|
||||
workspace_id: workspace_id.as_str().to_owned(),
|
||||
version: 1,
|
||||
status: OperationStatus::Draft,
|
||||
})
|
||||
@@ -260,12 +363,13 @@ impl AdminService {
|
||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), protocol = ?payload.operation.protocol))]
|
||||
pub async fn create_version(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
payload: NewVersionPayload,
|
||||
) -> Result<CreatedOperationResponse, ApiError> {
|
||||
self.validate_operation_payload(&payload.operation)?;
|
||||
|
||||
let summary = self.get_operation(operation_id).await?;
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
let now = now_string()?;
|
||||
let version = summary.current_draft_version + 1;
|
||||
let snapshot = RegistryOperation {
|
||||
@@ -295,6 +399,7 @@ impl AdminService {
|
||||
|
||||
self.registry
|
||||
.create_version(CreateVersionRequest {
|
||||
workspace_id,
|
||||
snapshot: &snapshot,
|
||||
change_note: payload.change_note.as_deref(),
|
||||
created_by: None,
|
||||
@@ -304,6 +409,7 @@ impl AdminService {
|
||||
|
||||
Ok(CreatedOperationResponse {
|
||||
operation_id: operation_id.as_str().to_owned(),
|
||||
workspace_id: workspace_id.as_str().to_owned(),
|
||||
version,
|
||||
status: OperationStatus::Draft,
|
||||
})
|
||||
@@ -312,12 +418,14 @@ impl AdminService {
|
||||
#[instrument(skip(self), fields(operation_id = %operation_id.as_str(), version))]
|
||||
pub async fn publish_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
version: u32,
|
||||
) -> Result<PublishResponse, ApiError> {
|
||||
let published_at = now_string()?;
|
||||
self.registry
|
||||
.publish_operation(PublishRequest {
|
||||
workspace_id,
|
||||
operation_id,
|
||||
version,
|
||||
published_at: &published_at,
|
||||
@@ -328,6 +436,7 @@ impl AdminService {
|
||||
|
||||
Ok(PublishResponse {
|
||||
operation_id: operation_id.as_str().to_owned(),
|
||||
workspace_id: workspace_id.as_str().to_owned(),
|
||||
published_version: version,
|
||||
published_at,
|
||||
})
|
||||
@@ -336,11 +445,12 @@ impl AdminService {
|
||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), version = payload.version))]
|
||||
pub async fn run_test(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
payload: TestRunPayload,
|
||||
) -> Result<TestRunResult, ApiError> {
|
||||
let record = self
|
||||
.get_operation_version(operation_id, payload.version)
|
||||
.get_operation_version(workspace_id, operation_id, payload.version)
|
||||
.await?;
|
||||
let runtime = RuntimeOperation::from(record.snapshot.clone());
|
||||
let request_preview =
|
||||
@@ -375,18 +485,23 @@ impl AdminService {
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_auth_profiles(&self) -> Result<Vec<AuthProfile>, ApiError> {
|
||||
Ok(self.registry.list_auth_profiles().await?)
|
||||
pub async fn list_auth_profiles(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Vec<AuthProfile>, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
Ok(self.registry.list_auth_profiles(workspace_id).await?)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("descriptor-set.bin")))]
|
||||
pub async fn upload_descriptor_set(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
source_name: Option<&str>,
|
||||
payload: &[u8],
|
||||
) -> Result<DescriptorUploadResponse, ApiError> {
|
||||
let summary = self.get_operation(operation_id).await?;
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
if summary.protocol != Protocol::Grpc {
|
||||
return Err(ApiError::validation(
|
||||
"descriptor upload is only allowed for grpc operations",
|
||||
@@ -441,11 +556,12 @@ impl AdminService {
|
||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("schema.proto")))]
|
||||
pub async fn upload_proto_file(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
source_name: Option<&str>,
|
||||
payload: &[u8],
|
||||
) -> Result<DescriptorUploadResponse, ApiError> {
|
||||
let summary = self.get_operation(operation_id).await?;
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
if summary.protocol != Protocol::Grpc {
|
||||
return Err(ApiError::validation(
|
||||
"proto upload is only allowed for grpc operations",
|
||||
@@ -495,10 +611,11 @@ impl AdminService {
|
||||
#[instrument(skip(self), fields(operation_id = %operation_id.as_str(), version))]
|
||||
pub async fn list_grpc_services(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
version: Option<u32>,
|
||||
) -> Result<Vec<GrpcServiceSummary>, ApiError> {
|
||||
let summary = self.get_operation(operation_id).await?;
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
if summary.protocol != Protocol::Grpc {
|
||||
return Err(ApiError::validation(
|
||||
"grpc services are only available for grpc operations",
|
||||
@@ -529,10 +646,11 @@ impl AdminService {
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_auth_profile(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
auth_profile_id: &AuthProfileId,
|
||||
) -> Result<AuthProfile, ApiError> {
|
||||
self.registry
|
||||
.get_auth_profile(auth_profile_id)
|
||||
.get_auth_profile(workspace_id, auth_profile_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!(
|
||||
@@ -545,13 +663,16 @@ impl AdminService {
|
||||
#[instrument(skip(self, payload), fields(auth_profile_name = %payload.name, auth_kind = ?payload.kind))]
|
||||
pub async fn create_auth_profile(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
payload: AuthProfilePayload,
|
||||
) -> Result<AuthProfile, ApiError> {
|
||||
validate_auth_profile_kind(payload.kind, &payload.config)?;
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
|
||||
let now = now_string()?;
|
||||
let profile = AuthProfile {
|
||||
id: AuthProfileId::new(new_prefixed_id("auth")),
|
||||
workspace_id: workspace_id.clone(),
|
||||
name: payload.name,
|
||||
kind: payload.kind,
|
||||
config: payload.config,
|
||||
@@ -560,7 +681,10 @@ impl AdminService {
|
||||
};
|
||||
|
||||
self.registry
|
||||
.save_auth_profile(SaveAuthProfileRequest { profile: &profile })
|
||||
.save_auth_profile(SaveAuthProfileRequest {
|
||||
workspace_id,
|
||||
profile: &profile,
|
||||
})
|
||||
.await?;
|
||||
info!(auth_profile_id = %profile.id.as_str(), "auth profile created");
|
||||
|
||||
@@ -570,18 +694,21 @@ impl AdminService {
|
||||
#[instrument(skip(self), fields(operation_id = %operation_id.as_str(), version = query.version.unwrap_or_default(), mode = ?query.mode))]
|
||||
pub async fn export_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
query: ExportQuery,
|
||||
) -> Result<String, ApiError> {
|
||||
let version = match query.version {
|
||||
Some(version) => version,
|
||||
None => {
|
||||
self.get_operation(operation_id)
|
||||
self.get_operation(workspace_id, operation_id)
|
||||
.await?
|
||||
.current_draft_version
|
||||
}
|
||||
};
|
||||
let record = self.get_operation_version(operation_id, version).await?;
|
||||
let record = self
|
||||
.get_operation_version(workspace_id, operation_id, version)
|
||||
.await?;
|
||||
let document = YamlOperationDocument {
|
||||
format_version: "1".to_owned(),
|
||||
kind: "operation".to_owned(),
|
||||
@@ -600,6 +727,7 @@ impl AdminService {
|
||||
#[instrument(skip(self, yaml_document), fields(mode = ?query.mode))]
|
||||
pub async fn import_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
query: ImportQuery,
|
||||
yaml_document: &str,
|
||||
) -> Result<ImportResponse, ApiError> {
|
||||
@@ -624,9 +752,10 @@ impl AdminService {
|
||||
|
||||
match query.mode {
|
||||
ImportMode::Create => {
|
||||
let created = self.create_operation(payload).await?;
|
||||
let created = self.create_operation(workspace_id, payload).await?;
|
||||
Ok(ImportResponse {
|
||||
operation_id: created.operation_id,
|
||||
workspace_id: created.workspace_id,
|
||||
version: created.version,
|
||||
import_mode: ImportMode::Create,
|
||||
warnings: Vec::new(),
|
||||
@@ -634,11 +763,12 @@ impl AdminService {
|
||||
}
|
||||
ImportMode::Upsert => {
|
||||
if let Some(existing) = self
|
||||
.find_operation_by_name(&document.operation.name)
|
||||
.find_operation_by_name(workspace_id, &document.operation.name)
|
||||
.await?
|
||||
{
|
||||
let created = self
|
||||
.create_version(
|
||||
workspace_id,
|
||||
&existing.id,
|
||||
NewVersionPayload {
|
||||
operation: payload,
|
||||
@@ -649,6 +779,7 @@ impl AdminService {
|
||||
|
||||
let response = ImportResponse {
|
||||
operation_id: created.operation_id,
|
||||
workspace_id: created.workspace_id,
|
||||
version: created.version,
|
||||
import_mode: ImportMode::Upsert,
|
||||
warnings: Vec::new(),
|
||||
@@ -660,9 +791,10 @@ impl AdminService {
|
||||
);
|
||||
Ok(response)
|
||||
} else {
|
||||
let created = self.create_operation(payload).await?;
|
||||
let created = self.create_operation(workspace_id, payload).await?;
|
||||
let response = ImportResponse {
|
||||
operation_id: created.operation_id,
|
||||
workspace_id: created.workspace_id,
|
||||
version: created.version,
|
||||
import_mode: ImportMode::Upsert,
|
||||
warnings: Vec::new(),
|
||||
@@ -681,11 +813,12 @@ impl AdminService {
|
||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), sample_kind = ?sample_kind))]
|
||||
pub async fn save_json_sample(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
sample_kind: SampleKind,
|
||||
payload: &Value,
|
||||
) -> Result<OperationSampleMetadata, ApiError> {
|
||||
let summary = self.get_operation(operation_id).await?;
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
let version = summary.current_draft_version;
|
||||
let sample_id = SampleId::new(new_prefixed_id("sample"));
|
||||
let now = now_string()?;
|
||||
@@ -725,10 +858,11 @@ impl AdminService {
|
||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str()))]
|
||||
pub async fn generate_draft(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
payload: GenerateDraftPayload,
|
||||
) -> Result<DraftGenerationResult, ApiError> {
|
||||
let summary = self.get_operation(operation_id).await?;
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
let samples = self
|
||||
.registry
|
||||
.list_sample_metadata(operation_id, summary.current_draft_version)
|
||||
@@ -795,15 +929,20 @@ impl AdminService {
|
||||
|
||||
async fn find_operation_by_name(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
name: &str,
|
||||
) -> Result<Option<OperationSummary>, ApiError> {
|
||||
Ok(self
|
||||
.registry
|
||||
.list_operations()
|
||||
.list_operations(workspace_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|operation| operation.name == name))
|
||||
}
|
||||
|
||||
async fn ensure_workspace_exists(&self, workspace_id: &WorkspaceId) -> Result<(), ApiError> {
|
||||
self.get_workspace(workspace_id).await.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_request_preview(
|
||||
|
||||
Reference in New Issue
Block a user