refactor: centralize workspace repository response DTOs
This commit is contained in:
@@ -2,19 +2,13 @@ use crate::{BackendApiClient, BackendApiClientError};
|
|||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use workspace_api::{RepositoryObservedStatus, RepositorySource};
|
use workspace_api::{WorkspaceCreateResponse, WorkspaceRepositoryRecord, WorkspaceSummary};
|
||||||
|
|
||||||
const DEFAULT_WORKSPACE_LIMIT: usize = 200;
|
const DEFAULT_WORKSPACE_LIMIT: usize = 200;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
pub type BackendWorkspace = WorkspaceSummary;
|
||||||
pub struct BackendWorkspace {
|
pub type CreateBackendWorkspaceResponse = WorkspaceCreateResponse;
|
||||||
pub workspace_id: String,
|
pub type CreateBackendWorkspaceRepositoryRecord = WorkspaceRepositoryRecord;
|
||||||
pub owner_account_id: Option<String>,
|
|
||||||
pub display_name: String,
|
|
||||||
pub state: String,
|
|
||||||
pub created_at: String,
|
|
||||||
pub updated_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
@@ -32,30 +26,6 @@ pub struct CreateBackendWorkspaceRepository {
|
|||||||
pub default_ref: Option<String>,
|
pub default_ref: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct CreateBackendWorkspaceResponse {
|
|
||||||
pub workspace: BackendWorkspace,
|
|
||||||
pub repository: CreateBackendWorkspaceRepositoryRecord,
|
|
||||||
pub config_revision: u64,
|
|
||||||
pub request_fingerprint: String,
|
|
||||||
pub replayed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct CreateBackendWorkspaceRepositoryRecord {
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub repository_id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub kind: String,
|
|
||||||
pub provider: Option<String>,
|
|
||||||
pub source: RepositorySource,
|
|
||||||
pub default_ref: Option<String>,
|
|
||||||
pub source_revision: u64,
|
|
||||||
pub source_fingerprint: String,
|
|
||||||
pub observed_status: RepositoryObservedStatus,
|
|
||||||
pub observed_at: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct BackendWorkspaceCatalogTarget {
|
pub struct BackendWorkspaceCatalogTarget {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
|
|||||||
@@ -14,5 +14,9 @@ serde = { workspace = true, features = ["derive"] }
|
|||||||
ts-rs = { version = "12.0.1", optional = true }
|
ts-rs = { version = "12.0.1", optional = true }
|
||||||
workdir.workspace = true
|
workdir.workspace = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "generate_typescript"
|
||||||
|
required-features = ["typescript"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
print!("{}", workspace_api::catalog_typescript());
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ use workdir::workspace::WorkingDirectorySummary;
|
|||||||
/// Local paths remain distinct from network Git transports so callers cannot
|
/// Local paths remain distinct from network Git transports so callers cannot
|
||||||
/// accidentally treat an unmaterialized remote as a server-local filesystem path.
|
/// accidentally treat an unmaterialized remote as a server-local filesystem path.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum RepositorySourceKind {
|
pub enum RepositorySourceKind {
|
||||||
LocalPath,
|
LocalPath,
|
||||||
@@ -55,6 +56,7 @@ impl RepositorySourceKind {
|
|||||||
|
|
||||||
/// Stable Repository source identity stored by Workspace authority.
|
/// Stable Repository source identity stored by Workspace authority.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
pub struct RepositorySource {
|
pub struct RepositorySource {
|
||||||
pub kind: RepositorySourceKind,
|
pub kind: RepositorySourceKind,
|
||||||
/// Canonical source representation. This is an absolute local path for
|
/// Canonical source representation. This is an absolute local path for
|
||||||
@@ -63,6 +65,7 @@ pub struct RepositorySource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum RepositoryObservedStatus {
|
pub enum RepositoryObservedStatus {
|
||||||
Unverified,
|
Unverified,
|
||||||
@@ -89,10 +92,216 @@ impl RepositoryObservedStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Public Workspace catalog item returned by `GET /api/workspaces`.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkspaceSummary {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub owner_account_id: Option<String>,
|
||||||
|
pub display_name: String,
|
||||||
|
pub state: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public Repository record embedded in Workspace creation responses.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkspaceRepositoryRecord {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub repository_id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub kind: String,
|
||||||
|
pub provider: Option<String>,
|
||||||
|
pub source: RepositorySource,
|
||||||
|
pub default_ref: Option<String>,
|
||||||
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
pub source_revision: u64,
|
||||||
|
pub source_fingerprint: String,
|
||||||
|
pub observed_status: RepositoryObservedStatus,
|
||||||
|
pub observed_at: Option<String>,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response returned after atomically creating a Workspace and its first Repository.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkspaceCreateResponse {
|
||||||
|
pub workspace: WorkspaceSummary,
|
||||||
|
pub repository: WorkspaceRepositoryRecord,
|
||||||
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
pub config_revision: u64,
|
||||||
|
pub request_fingerprint: String,
|
||||||
|
pub replayed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browser authentication configuration exposed by the scoped Workspace summary.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub enum WorkspaceAuthConfig {
|
||||||
|
Passkey {
|
||||||
|
rp_id: String,
|
||||||
|
origin: String,
|
||||||
|
public_base_url: String,
|
||||||
|
cookie_name: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Backend-authoritative permissions for the current Workspace actor.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkspacePermissionSummary {
|
||||||
|
pub manage_repositories: bool,
|
||||||
|
pub manage_secrets: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkspaceExtensionPointState {
|
||||||
|
pub status: String,
|
||||||
|
pub note: String,
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkspaceExtensionPoints {
|
||||||
|
pub store: String,
|
||||||
|
pub event_stream: WorkspaceExtensionPointState,
|
||||||
|
pub host_worker_bridge: WorkspaceExtensionPointState,
|
||||||
|
pub companion_console: WorkspaceExtensionPointState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scoped Workspace metadata and current-actor permission projection.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkspaceResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub record_authority: String,
|
||||||
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
pub schema_version: i64,
|
||||||
|
pub auth: WorkspaceAuthConfig,
|
||||||
|
pub permissions: WorkspacePermissionSummary,
|
||||||
|
pub extension_points: WorkspaceExtensionPoints,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct RepositoryDiagnostic {
|
||||||
|
pub severity: String,
|
||||||
|
pub code: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct GitRemoteSummary {
|
||||||
|
pub name: String,
|
||||||
|
pub fetch_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct GitRepositorySummary {
|
||||||
|
pub status: String,
|
||||||
|
pub head: Option<String>,
|
||||||
|
pub branch: Option<String>,
|
||||||
|
pub dirty: bool,
|
||||||
|
pub remotes: Vec<GitRemoteSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct RepositorySummary {
|
||||||
|
pub id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub kind: String,
|
||||||
|
pub provider: String,
|
||||||
|
pub source: RepositorySource,
|
||||||
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
pub source_revision: u64,
|
||||||
|
pub source_fingerprint: String,
|
||||||
|
pub observed_status: RepositoryObservedStatus,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
|
||||||
|
pub observed_at: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
|
||||||
|
pub default_selector: Option<String>,
|
||||||
|
pub record_authority: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
|
||||||
|
pub git: Option<GitRepositorySummary>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
|
||||||
|
pub diagnostics: Option<Vec<RepositoryDiagnostic>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct GitCommitSummary {
|
||||||
|
pub hash: String,
|
||||||
|
pub short_hash: String,
|
||||||
|
pub summary: String,
|
||||||
|
pub author_name: String,
|
||||||
|
pub author_email: String,
|
||||||
|
pub author_date: String,
|
||||||
|
pub parents: Vec<String>,
|
||||||
|
pub refs: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct RepositoryListResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub items: Vec<RepositorySummary>,
|
||||||
|
pub source: String,
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct RepositoryDetailResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub item: RepositorySummary,
|
||||||
|
pub source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct RepositoryLogResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub repository_id: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
|
||||||
|
pub default_selector: Option<String>,
|
||||||
|
pub limit: usize,
|
||||||
|
pub items: Vec<GitCommitSummary>,
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
pub const TICKET_RELATIONS_QUERY_PATH: &str = "/tickets/relations/search";
|
pub const TICKET_RELATIONS_QUERY_PATH: &str = "/tickets/relations/search";
|
||||||
pub const TICKET_ORCHESTRATION_PLANS_QUERY_PATH: &str = "/tickets/orchestration-plans/search";
|
pub const TICKET_ORCHESTRATION_PLANS_QUERY_PATH: &str = "/tickets/orchestration-plans/search";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum DiagnosticSeverity {
|
pub enum DiagnosticSeverity {
|
||||||
Info,
|
Info,
|
||||||
@@ -101,6 +310,7 @@ pub enum DiagnosticSeverity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
pub struct Diagnostic {
|
pub struct Diagnostic {
|
||||||
pub code: String,
|
pub code: String,
|
||||||
pub severity: DiagnosticSeverity,
|
pub severity: DiagnosticSeverity,
|
||||||
@@ -537,10 +747,127 @@ pub struct RepositoryAccessProjection {
|
|||||||
pub bindings: Vec<RepositorySshAccessBinding>,
|
pub bindings: Vec<RepositorySshAccessBinding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "typescript")]
|
||||||
|
pub fn catalog_typescript() -> String {
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
let config = ts_rs::Config::default();
|
||||||
|
let declarations = [
|
||||||
|
WorkspaceSummary::decl(&config),
|
||||||
|
WorkspaceRepositoryRecord::decl(&config),
|
||||||
|
WorkspaceCreateResponse::decl(&config),
|
||||||
|
WorkspaceAuthConfig::decl(&config),
|
||||||
|
WorkspacePermissionSummary::decl(&config),
|
||||||
|
DiagnosticSeverity::decl(&config),
|
||||||
|
Diagnostic::decl(&config),
|
||||||
|
WorkspaceExtensionPointState::decl(&config),
|
||||||
|
WorkspaceExtensionPoints::decl(&config),
|
||||||
|
WorkspaceResponse::decl(&config),
|
||||||
|
RepositorySourceKind::decl(&config),
|
||||||
|
RepositorySource::decl(&config),
|
||||||
|
RepositoryObservedStatus::decl(&config),
|
||||||
|
RepositoryDiagnostic::decl(&config),
|
||||||
|
GitRemoteSummary::decl(&config),
|
||||||
|
GitRepositorySummary::decl(&config),
|
||||||
|
RepositorySummary::decl(&config),
|
||||||
|
GitCommitSummary::decl(&config),
|
||||||
|
RepositoryListResponse::decl(&config),
|
||||||
|
RepositoryDetailResponse::decl(&config),
|
||||||
|
RepositoryLogResponse::decl(&config),
|
||||||
|
]
|
||||||
|
.map(|declaration| format!("export {declaration}"));
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"// This file is generated by `cargo run -p workspace-api --features typescript --example generate_typescript | deno fmt -`.\n// Do not edit this file directly.\n\n{}\n",
|
||||||
|
declarations.join("\n\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_and_repository_response_shapes_round_trip() {
|
||||||
|
let workspace = serde_json::json!({
|
||||||
|
"workspace_id": "workspace-test",
|
||||||
|
"display_name": "Test",
|
||||||
|
"record_authority": "workspace-control-plane",
|
||||||
|
"schema_version": 46,
|
||||||
|
"auth": {"Passkey": {
|
||||||
|
"rp_id": "example.test",
|
||||||
|
"origin": "https://example.test",
|
||||||
|
"public_base_url": "https://example.test",
|
||||||
|
"cookie_name": "yoi_session"
|
||||||
|
}},
|
||||||
|
"permissions": {
|
||||||
|
"manage_repositories": true,
|
||||||
|
"manage_secrets": true
|
||||||
|
},
|
||||||
|
"extension_points": {
|
||||||
|
"store": "sqlite",
|
||||||
|
"event_stream": {"status": "available", "note": "ready", "diagnostics": []},
|
||||||
|
"host_worker_bridge": {"status": "available", "note": "ready", "diagnostics": []},
|
||||||
|
"companion_console": {"status": "available", "note": "ready", "diagnostics": []}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let parsed: WorkspaceResponse = serde_json::from_value(workspace.clone()).unwrap();
|
||||||
|
assert_eq!(serde_json::to_value(parsed).unwrap(), workspace);
|
||||||
|
|
||||||
|
let repositories = serde_json::json!({
|
||||||
|
"workspace_id": "workspace-test",
|
||||||
|
"items": [{
|
||||||
|
"id": "main",
|
||||||
|
"display_name": "main",
|
||||||
|
"kind": "git",
|
||||||
|
"provider": "git",
|
||||||
|
"source": {"kind": "local_path", "uri": "/srv/project"},
|
||||||
|
"source_revision": 1,
|
||||||
|
"source_fingerprint": "sha256:test",
|
||||||
|
"observed_status": "ready",
|
||||||
|
"record_authority": "workspace-control-plane"
|
||||||
|
}],
|
||||||
|
"source": "workspace-control-plane",
|
||||||
|
"diagnostics": []
|
||||||
|
});
|
||||||
|
let parsed: RepositoryListResponse = serde_json::from_value(repositories.clone()).unwrap();
|
||||||
|
assert_eq!(serde_json::to_value(parsed).unwrap(), repositories);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repository_response_rejects_stale_field_aliases() {
|
||||||
|
let stale = serde_json::json!({
|
||||||
|
"workspace_id": "workspace-test",
|
||||||
|
"items": [{
|
||||||
|
"repository_id": "main",
|
||||||
|
"display_name": "main",
|
||||||
|
"kind": "git",
|
||||||
|
"provider": "git",
|
||||||
|
"source": {"kind": "local_path", "uri": "/srv/project"},
|
||||||
|
"source_revision": 1,
|
||||||
|
"source_fingerprint": "sha256:test",
|
||||||
|
"observed_status": "ready",
|
||||||
|
"record_authority": "workspace-control-plane"
|
||||||
|
}],
|
||||||
|
"source": "workspace-control-plane",
|
||||||
|
"diagnostics": []
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(serde_json::from_value::<RepositoryListResponse>(stale).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "typescript")]
|
||||||
|
#[test]
|
||||||
|
fn generated_catalog_typescript_keeps_public_wrappers_and_nullability() {
|
||||||
|
let output = catalog_typescript();
|
||||||
|
assert!(output.contains("export type WorkspaceResponse ="));
|
||||||
|
assert!(output.contains("permissions: WorkspacePermissionSummary"));
|
||||||
|
assert!(output.contains("export type RepositoryListResponse ="));
|
||||||
|
assert!(output.contains("items: Array<RepositorySummary>"));
|
||||||
|
assert!(output.contains("observed_at?: string | null"));
|
||||||
|
assert!(!output.contains("repository_id: string, display_name"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn worker_resource_key_is_required() {
|
fn worker_resource_key_is_required() {
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
|
|||||||
@@ -42,19 +42,19 @@ pub use authority::{
|
|||||||
pub use config::{BackendRuntimesConfigFile, ResolvedWorkspaceBackendConfig, ServerHostConfigFile};
|
pub use config::{BackendRuntimesConfigFile, ResolvedWorkspaceBackendConfig, ServerHostConfigFile};
|
||||||
pub use identity::{WORKSPACE_IDENTITY_RELATIVE_PATH, WorkspaceIdentity};
|
pub use identity::{WORKSPACE_IDENTITY_RELATIVE_PATH, WorkspaceIdentity};
|
||||||
pub use records::{ObjectiveDetail, ObjectiveSummary, TicketDetail, TicketSummary};
|
pub use records::{ObjectiveDetail, ObjectiveSummary, TicketDetail, TicketSummary};
|
||||||
pub use repositories::{
|
pub use repositories::{ConfiguredRepository, RepositoryLogRead, RepositoryRegistryReader};
|
||||||
ConfiguredRepository, GitCommitSummary, GitRemoteSummary, GitRepositorySummary,
|
|
||||||
RepositoryLogRead, RepositoryRegistryReader, RepositorySummary,
|
|
||||||
};
|
|
||||||
pub use server::{
|
pub use server::{
|
||||||
AuthConfig, ServerConfig, WorkspaceApi, WorkspaceServerApi, build_router,
|
AuthConfig, ServerConfig, WorkspaceApi, WorkspaceServerApi, build_router,
|
||||||
build_workspace_server_router, serve, serve_workspace_catalog,
|
build_workspace_server_router, serve, serve_workspace_catalog,
|
||||||
};
|
};
|
||||||
pub use store::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord};
|
pub use store::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord};
|
||||||
pub use workspace_catalog::{
|
pub use workspace_api::{
|
||||||
InitialRepositoryIntent, WorkspaceCatalogService, WorkspaceCreateRequest,
|
GitCommitSummary, GitRemoteSummary, GitRepositorySummary, RepositorySummary,
|
||||||
WorkspaceCreateResponse,
|
WorkspaceCreateResponse,
|
||||||
};
|
};
|
||||||
|
pub use workspace_catalog::{
|
||||||
|
InitialRepositoryIntent, WorkspaceCatalogService, WorkspaceCreateRequest, WorkspaceCreateResult,
|
||||||
|
};
|
||||||
|
|
||||||
use worker_runtime::identity::RuntimeWorkerRef;
|
use worker_runtime::identity::RuntimeWorkerRef;
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use std::{
|
|||||||
process::Command,
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use workspace_api::{
|
||||||
use workspace_api::{RepositoryObservedStatus, RepositorySource};
|
Diagnostic, DiagnosticSeverity, GitCommitSummary, GitRemoteSummary, GitRepositorySummary,
|
||||||
|
RepositoryDiagnostic, RepositoryObservedStatus, RepositorySource, RepositorySummary,
|
||||||
|
};
|
||||||
|
|
||||||
pub type RepositoryId = String;
|
pub type RepositoryId = String;
|
||||||
pub type RepositorySelector = String;
|
pub type RepositorySelector = String;
|
||||||
@@ -24,74 +26,19 @@ pub struct ConfiguredRepository {
|
|||||||
pub default_selector: Option<RepositorySelector>,
|
pub default_selector: Option<RepositorySelector>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct RepositorySummary {
|
|
||||||
pub id: RepositoryId,
|
|
||||||
pub display_name: String,
|
|
||||||
pub kind: String,
|
|
||||||
pub provider: String,
|
|
||||||
pub source: RepositorySource,
|
|
||||||
pub source_revision: u64,
|
|
||||||
pub source_fingerprint: String,
|
|
||||||
pub observed_status: RepositoryObservedStatus,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub observed_at: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub default_selector: Option<RepositorySelector>,
|
|
||||||
pub record_authority: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub git: Option<GitRepositorySummary>,
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
||||||
pub diagnostics: Vec<RepositoryDiagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct GitRepositorySummary {
|
|
||||||
pub status: String,
|
|
||||||
pub head: Option<String>,
|
|
||||||
pub branch: Option<String>,
|
|
||||||
pub dirty: bool,
|
|
||||||
pub remotes: Vec<GitRemoteSummary>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct GitRemoteSummary {
|
|
||||||
pub name: String,
|
|
||||||
pub fetch_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct RepositoryDiagnostic {
|
|
||||||
pub severity: String,
|
|
||||||
pub code: String,
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct RepositoryListProjection {
|
pub struct RepositoryListProjection {
|
||||||
pub items: Vec<RepositorySummary>,
|
pub items: Vec<RepositorySummary>,
|
||||||
pub diagnostics: Vec<RepositoryDiagnostic>,
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct RepositoryLogRead {
|
pub struct RepositoryLogRead {
|
||||||
pub repository_id: RepositoryId,
|
pub repository_id: RepositoryId,
|
||||||
pub default_selector: Option<RepositorySelector>,
|
pub default_selector: Option<RepositorySelector>,
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
pub commits: Vec<GitCommitSummary>,
|
pub commits: Vec<GitCommitSummary>,
|
||||||
pub diagnostics: Vec<RepositoryDiagnostic>,
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct GitCommitSummary {
|
|
||||||
pub hash: String,
|
|
||||||
pub short_hash: String,
|
|
||||||
pub summary: String,
|
|
||||||
pub author_name: String,
|
|
||||||
pub author_email: String,
|
|
||||||
pub author_date: String,
|
|
||||||
pub parents: Vec<String>,
|
|
||||||
pub refs: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -131,8 +78,8 @@ impl RepositoryRegistryReader {
|
|||||||
if self.repositories.is_empty() {
|
if self.repositories.is_empty() {
|
||||||
return RepositoryListProjection {
|
return RepositoryListProjection {
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
diagnostics: vec![RepositoryDiagnostic {
|
diagnostics: vec![Diagnostic {
|
||||||
severity: "warning".to_string(),
|
severity: DiagnosticSeverity::Warning,
|
||||||
code: "repository_config_empty".to_string(),
|
code: "repository_config_empty".to_string(),
|
||||||
message: "No repositories are configured for this workspace backend."
|
message: "No repositories are configured for this workspace backend."
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -177,8 +124,8 @@ impl RepositoryRegistryReader {
|
|||||||
let commits = match self.git_log(repository, limit) {
|
let commits = match self.git_log(repository, limit) {
|
||||||
Ok(commits) => commits,
|
Ok(commits) => commits,
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
diagnostics.push(RepositoryDiagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
severity: "warning".to_string(),
|
severity: DiagnosticSeverity::Warning,
|
||||||
code: "repository_git_log_unavailable".to_string(),
|
code: "repository_git_log_unavailable".to_string(),
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
@@ -379,7 +326,7 @@ impl RepositoryRegistryReader {
|
|||||||
default_selector: repository.default_selector.clone(),
|
default_selector: repository.default_selector.clone(),
|
||||||
record_authority: "workspace-control-plane".to_string(),
|
record_authority: "workspace-control-plane".to_string(),
|
||||||
git,
|
git,
|
||||||
diagnostics,
|
diagnostics: (!diagnostics.is_empty()).then_some(diagnostics),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,7 +622,10 @@ mod tests {
|
|||||||
RepositoryObservedStatus::Unverified
|
RepositoryObservedStatus::Unverified
|
||||||
);
|
);
|
||||||
assert!(summary.git.is_none());
|
assert!(summary.git.is_none());
|
||||||
assert_eq!(summary.diagnostics[0].code, "repository_source_unverified");
|
assert_eq!(
|
||||||
|
summary.diagnostics.as_ref().unwrap()[0].code,
|
||||||
|
"repository_source_unverified"
|
||||||
|
);
|
||||||
|
|
||||||
let repository = reader.merge_repository("remote").unwrap();
|
let repository = reader.merge_repository("remote").unwrap();
|
||||||
let error = merge_git_stdout(&repository, "inspect", &["rev-parse", "HEAD"]).unwrap_err();
|
let error = merge_git_stdout(&repository, "inspect", &["rev-parse", "HEAD"]).unwrap_err();
|
||||||
|
|||||||
@@ -64,9 +64,12 @@ use workspace_api::{
|
|||||||
DeleteRepositorySshCredentialRequest, DeleteRepositorySshHostTrustRequest,
|
DeleteRepositorySshCredentialRequest, DeleteRepositorySshHostTrustRequest,
|
||||||
ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest,
|
ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest,
|
||||||
ObjectiveStateRequest, PutRepositorySshHostTrustRequest, RepositoryAccessProjection,
|
ObjectiveStateRequest, PutRepositorySshHostTrustRequest, RepositoryAccessProjection,
|
||||||
|
RepositoryDetailResponse, RepositoryListResponse, RepositoryLogResponse,
|
||||||
RepositorySshCredential, RepositorySshHostTrust, RotateRepositorySshCredentialRequest,
|
RepositorySshCredential, RepositorySshHostTrust, RotateRepositorySshCredentialRequest,
|
||||||
RuntimeConnectionTestResponse, RuntimeManagementSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
|
RuntimeConnectionTestResponse, RuntimeManagementSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
|
||||||
TICKET_RELATIONS_QUERY_PATH, WorkspaceRuntimeResource, WorkspaceWorkerDiscoveryItem,
|
TICKET_RELATIONS_QUERY_PATH, WorkspaceCreateResponse, WorkspaceExtensionPointState,
|
||||||
|
WorkspaceExtensionPoints, WorkspacePermissionSummary, WorkspaceRepositoryRecord,
|
||||||
|
WorkspaceResponse, WorkspaceRuntimeResource, WorkspaceSummary, WorkspaceWorkerDiscoveryItem,
|
||||||
WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject,
|
WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,7 +118,7 @@ use crate::records::{
|
|||||||
};
|
};
|
||||||
use crate::repositories::{
|
use crate::repositories::{
|
||||||
ConfiguredRepository, RepositoryListProjection, RepositoryLogRead, RepositoryLookupError,
|
ConfiguredRepository, RepositoryListProjection, RepositoryLogRead, RepositoryLookupError,
|
||||||
RepositoryRegistryReader, RepositorySummary,
|
RepositoryRegistryReader,
|
||||||
};
|
};
|
||||||
use crate::repository_access::{
|
use crate::repository_access::{
|
||||||
RepositoryAccessConfigSchemaProvider, RepositorySecretService,
|
RepositoryAccessConfigSchemaProvider, RepositorySecretService,
|
||||||
@@ -150,17 +153,7 @@ use worker_runtime::identity::{RuntimeWorkerRef, WorkerId};
|
|||||||
|
|
||||||
const EMBEDDED_WORKER_RUNTIME_ID: &str = "embedded-worker-runtime";
|
const EMBEDDED_WORKER_RUNTIME_ID: &str = "embedded-worker-runtime";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
pub use workspace_api::WorkspaceAuthConfig as AuthConfig;
|
||||||
pub enum AuthConfig {
|
|
||||||
/// Browser human auth uses Passkey ceremonies and HttpOnly cookie sessions;
|
|
||||||
/// CLI/TUI auth uses API tokens obtained through the device login flow.
|
|
||||||
Passkey {
|
|
||||||
rp_id: String,
|
|
||||||
origin: String,
|
|
||||||
public_base_url: String,
|
|
||||||
cookie_name: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
@@ -987,7 +980,7 @@ async fn list_server_workspaces(
|
|||||||
let owner = match resolve_server_actor(&api, &headers).await {
|
let owner = match resolve_server_actor(&api, &headers).await {
|
||||||
Ok(Some(actor)) => Some(actor.account_id),
|
Ok(Some(actor)) => Some(actor.account_id),
|
||||||
Ok(None) => match api.catalog.is_empty() {
|
Ok(None) => match api.catalog.is_empty() {
|
||||||
Ok(true) => return Json(Vec::<WorkspaceRecord>::new()).into_response(),
|
Ok(true) => return Json(Vec::<WorkspaceSummary>::new()).into_response(),
|
||||||
Ok(false) => return StatusCode::UNAUTHORIZED.into_response(),
|
Ok(false) => return StatusCode::UNAUTHORIZED.into_response(),
|
||||||
Err(error) => return server_error_response(error),
|
Err(error) => return server_error_response(error),
|
||||||
},
|
},
|
||||||
@@ -997,7 +990,13 @@ async fn list_server_workspaces(
|
|||||||
.catalog
|
.catalog
|
||||||
.list(owner.as_deref(), query.limit.unwrap_or(100))
|
.list(owner.as_deref(), query.limit.unwrap_or(100))
|
||||||
{
|
{
|
||||||
Ok(workspaces) => Json(workspaces).into_response(),
|
Ok(workspaces) => Json(
|
||||||
|
workspaces
|
||||||
|
.into_iter()
|
||||||
|
.map(workspace_summary)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
Err(error) => server_error_response(error),
|
Err(error) => server_error_response(error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1029,7 +1028,48 @@ async fn create_server_workspace(
|
|||||||
} else {
|
} else {
|
||||||
StatusCode::CREATED
|
StatusCode::CREATED
|
||||||
};
|
};
|
||||||
(status, Json(created)).into_response()
|
(status, Json(workspace_create_response(created))).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_summary(record: WorkspaceRecord) -> WorkspaceSummary {
|
||||||
|
WorkspaceSummary {
|
||||||
|
workspace_id: record.workspace_id,
|
||||||
|
owner_account_id: record.owner_account_id,
|
||||||
|
display_name: record.display_name,
|
||||||
|
state: record.state,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_repository_record(record: RepositoryRecord) -> WorkspaceRepositoryRecord {
|
||||||
|
WorkspaceRepositoryRecord {
|
||||||
|
workspace_id: record.workspace_id,
|
||||||
|
repository_id: record.repository_id,
|
||||||
|
name: record.name,
|
||||||
|
kind: record.kind,
|
||||||
|
provider: record.provider,
|
||||||
|
source: record.source,
|
||||||
|
default_ref: record.default_ref,
|
||||||
|
source_revision: record.source_revision,
|
||||||
|
source_fingerprint: record.source_fingerprint,
|
||||||
|
observed_status: record.observed_status,
|
||||||
|
observed_at: record.observed_at,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_create_response(
|
||||||
|
created: crate::workspace_catalog::WorkspaceCreateResult,
|
||||||
|
) -> WorkspaceCreateResponse {
|
||||||
|
WorkspaceCreateResponse {
|
||||||
|
workspace: workspace_summary(created.workspace),
|
||||||
|
repository: workspace_repository_record(created.repository),
|
||||||
|
config_revision: created.config_revision,
|
||||||
|
request_fingerprint: created.request_fingerprint,
|
||||||
|
replayed: created.replayed,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn resolve_server_actor(
|
async fn resolve_server_actor(
|
||||||
@@ -2734,31 +2774,6 @@ pub async fn serve(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct WorkspaceResponse {
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub display_name: String,
|
|
||||||
pub record_authority: String,
|
|
||||||
pub schema_version: i64,
|
|
||||||
pub auth: AuthConfig,
|
|
||||||
pub extension_points: ExtensionPoints,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct ExtensionPoints {
|
|
||||||
pub store: String,
|
|
||||||
pub event_stream: ExtensionPointState,
|
|
||||||
pub host_worker_bridge: ExtensionPointState,
|
|
||||||
pub companion_console: ExtensionPointState,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct ExtensionPointState {
|
|
||||||
pub status: String,
|
|
||||||
pub note: String,
|
|
||||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ListResponse<T> {
|
pub struct ListResponse<T> {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
@@ -3062,32 +3077,6 @@ pub struct BrowserCreateWorkerResponse {
|
|||||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct RepositoryListResponse {
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub items: Vec<RepositorySummary>,
|
|
||||||
pub source: String,
|
|
||||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct RepositoryDetailResponse {
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub item: RepositorySummary,
|
|
||||||
pub source: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct RepositoryLogResponse {
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub repository_id: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub default_selector: Option<String>,
|
|
||||||
pub limit: usize,
|
|
||||||
pub items: Vec<crate::repositories::GitCommitSummary>,
|
|
||||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct LogQuery {
|
struct LogQuery {
|
||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
@@ -3352,11 +3341,12 @@ async fn scoped_get_flow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn scoped_get_workspace(
|
async fn scoped_get_workspace(
|
||||||
|
headers: HeaderMap,
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
) -> ApiResult<Json<WorkspaceResponse>> {
|
) -> ApiResult<Json<WorkspaceResponse>> {
|
||||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
get_workspace(State(api)).await
|
get_workspace(headers, State(api)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn scoped_get_workspace_settings(
|
async fn scoped_get_workspace_settings(
|
||||||
@@ -11051,9 +11041,20 @@ async fn require_actor(api: &ServerAuthApi, headers: &HeaderMap) -> ApiResult<Re
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<WorkspaceResponse>> {
|
async fn get_workspace(
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
) -> ApiResult<Json<WorkspaceResponse>> {
|
||||||
|
let cookie_name = auth_public_config(&api.config).cookie_name;
|
||||||
|
let actor = resolve_request_actor(api.store.as_ref(), &headers, &cookie_name).await?;
|
||||||
let schema_version = api.store.schema_version().await?;
|
let schema_version = api.store.schema_version().await?;
|
||||||
let stored = api.store.get_workspace(api.workspace_id()).await?;
|
let stored = api.store.get_workspace(api.workspace_id()).await?;
|
||||||
|
let is_owner = actor.as_ref().is_some_and(|actor| {
|
||||||
|
stored
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|workspace| workspace.owner_account_id.as_ref())
|
||||||
|
== Some(&actor.account_id)
|
||||||
|
});
|
||||||
let display_name = stored
|
let display_name = stored
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|record| record.display_name.clone())
|
.map(|record| record.display_name.clone())
|
||||||
@@ -11066,14 +11067,18 @@ async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<Worksp
|
|||||||
record_authority: "local_yoi_project_records".to_string(),
|
record_authority: "local_yoi_project_records".to_string(),
|
||||||
schema_version,
|
schema_version,
|
||||||
auth: api.config.auth.clone(),
|
auth: api.config.auth.clone(),
|
||||||
extension_points: ExtensionPoints {
|
permissions: WorkspacePermissionSummary {
|
||||||
|
manage_repositories: is_owner,
|
||||||
|
manage_secrets: is_owner,
|
||||||
|
},
|
||||||
|
extension_points: WorkspaceExtensionPoints {
|
||||||
store: "sqlite".to_string(),
|
store: "sqlite".to_string(),
|
||||||
event_stream: ExtensionPointState {
|
event_stream: WorkspaceExtensionPointState {
|
||||||
status: "backend_proxy".to_string(),
|
status: "backend_proxy".to_string(),
|
||||||
note: "Worker observation streams are exposed only through the Workspace server proxy keyed by runtime_id + worker_id; browser clients never receive raw Runtime endpoints or socket paths.".to_string(),
|
note: "Worker observation streams are exposed only through the Workspace server proxy keyed by runtime_id + worker_id; browser clients never receive raw Runtime endpoints or socket paths.".to_string(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
},
|
},
|
||||||
host_worker_bridge: ExtensionPointState {
|
host_worker_bridge: WorkspaceExtensionPointState {
|
||||||
status: "runtime_registry".to_string(),
|
status: "runtime_registry".to_string(),
|
||||||
note: "Hosts and Workers are projected from the Workspace RuntimeRegistry; raw Runtime endpoints, sockets, and local metadata paths are not exposed.".to_string(),
|
note: "Hosts and Workers are projected from the Workspace RuntimeRegistry; raw Runtime endpoints, sockets, and local metadata paths are not exposed.".to_string(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
@@ -11083,7 +11088,9 @@ async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<Worksp
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn companion_console_extension_point(status: &CompanionStatusResponse) -> ExtensionPointState {
|
fn companion_console_extension_point(
|
||||||
|
status: &CompanionStatusResponse,
|
||||||
|
) -> WorkspaceExtensionPointState {
|
||||||
let completion = status.transport.completion.clone();
|
let completion = status.transport.completion.clone();
|
||||||
let note = match completion.as_str() {
|
let note = match completion.as_str() {
|
||||||
"connected" => "Workspace Companion is input-capable and browser input is dispatched through the normal Worker runtime path.".to_string(),
|
"connected" => "Workspace Companion is input-capable and browser input is dispatched through the normal Worker runtime path.".to_string(),
|
||||||
@@ -11107,10 +11114,10 @@ fn companion_console_extension_point(status: &CompanionStatusResponse) -> Extens
|
|||||||
"Workspace Companion transport reports {other}; browser input follows the Companion Worker runtime capability state."
|
"Workspace Companion transport reports {other}; browser input follows the Companion Worker runtime capability state."
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
ExtensionPointState {
|
WorkspaceExtensionPointState {
|
||||||
status: completion,
|
status: completion,
|
||||||
note,
|
note,
|
||||||
diagnostics: status.diagnostics.clone(),
|
diagnostics: status.diagnostics.iter().cloned().map(Into::into).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11193,7 +11200,7 @@ async fn list_repositories(
|
|||||||
workspace_id: api.config.workspace_id,
|
workspace_id: api.config.workspace_id,
|
||||||
items,
|
items,
|
||||||
source: "workspace-control-plane".to_string(),
|
source: "workspace-control-plane".to_string(),
|
||||||
diagnostics: repository_diagnostics(diagnostics),
|
diagnostics,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11230,7 +11237,7 @@ async fn repository_log(
|
|||||||
default_selector,
|
default_selector,
|
||||||
limit,
|
limit,
|
||||||
items: commits,
|
items: commits,
|
||||||
diagnostics: repository_diagnostics(diagnostics),
|
diagnostics,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14937,23 +14944,6 @@ fn sanitize_backend_error(message: &str) -> String {
|
|||||||
message.to_string()
|
message.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repository_diagnostics(
|
|
||||||
diagnostics: Vec<crate::repositories::RepositoryDiagnostic>,
|
|
||||||
) -> Vec<RuntimeDiagnostic> {
|
|
||||||
diagnostics
|
|
||||||
.into_iter()
|
|
||||||
.map(|diagnostic| RuntimeDiagnostic {
|
|
||||||
code: diagnostic.code,
|
|
||||||
severity: match diagnostic.severity.as_str() {
|
|
||||||
"error" => DiagnosticSeverity::Error,
|
|
||||||
"warning" => DiagnosticSeverity::Warning,
|
|
||||||
_ => DiagnosticSeverity::Info,
|
|
||||||
},
|
|
||||||
message: diagnostic.message,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn repository_lookup<T>(result: std::result::Result<T, RepositoryLookupError>) -> ApiResult<T> {
|
fn repository_lookup<T>(result: std::result::Result<T, RepositoryLookupError>) -> ApiResult<T> {
|
||||||
result.map_err(|error| match error {
|
result.map_err(|error| match error {
|
||||||
RepositoryLookupError::UnknownRepository { id } => {
|
RepositoryLookupError::UnknownRepository { id } => {
|
||||||
@@ -17761,6 +17751,12 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(authenticated_legacy.status(), StatusCode::OK);
|
assert_eq!(authenticated_legacy.status(), StatusCode::OK);
|
||||||
|
let workspace_body = to_bytes(authenticated_legacy.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let typed_workspace: workspace_api::WorkspaceResponse =
|
||||||
|
serde_json::from_slice(&workspace_body).unwrap();
|
||||||
|
assert!(typed_workspace.permissions.manage_repositories);
|
||||||
|
|
||||||
let anonymous_catalog = app
|
let anonymous_catalog = app
|
||||||
.clone()
|
.clone()
|
||||||
@@ -17785,6 +17781,15 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(authenticated_catalog.status(), StatusCode::OK);
|
assert_eq!(authenticated_catalog.status(), StatusCode::OK);
|
||||||
|
let catalog_body = to_bytes(authenticated_catalog.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let typed_catalog: Vec<workspace_api::WorkspaceSummary> =
|
||||||
|
serde_json::from_slice(&catalog_body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
typed_catalog[0].workspace_id,
|
||||||
|
workspace.workspace.workspace_id
|
||||||
|
);
|
||||||
|
|
||||||
for path in ["/api/workspaces", "/api/auth/device-login/approve"] {
|
for path in ["/api/workspaces", "/api/auth/device-login/approve"] {
|
||||||
let cross_site = app
|
let cross_site = app
|
||||||
@@ -23865,6 +23870,9 @@ mod tests {
|
|||||||
let workspace = get_json(app.clone(), "/api/workspace").await;
|
let workspace = get_json(app.clone(), "/api/workspace").await;
|
||||||
assert_eq!(workspace["workspace_id"], TEST_WORKSPACE_ID);
|
assert_eq!(workspace["workspace_id"], TEST_WORKSPACE_ID);
|
||||||
assert_eq!(workspace["display_name"], "Test Workspace");
|
assert_eq!(workspace["display_name"], "Test Workspace");
|
||||||
|
let typed_workspace: workspace_api::WorkspaceResponse =
|
||||||
|
serde_json::from_value(workspace.clone()).unwrap();
|
||||||
|
assert!(!typed_workspace.permissions.manage_repositories);
|
||||||
assert_eq!(workspace["record_authority"], "local_yoi_project_records");
|
assert_eq!(workspace["record_authority"], "local_yoi_project_records");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
workspace["extension_points"]["host_worker_bridge"]["status"],
|
workspace["extension_points"]["host_worker_bridge"]["status"],
|
||||||
@@ -24051,6 +24059,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let repositories = get_json(app.clone(), "/api/repositories").await;
|
let repositories = get_json(app.clone(), "/api/repositories").await;
|
||||||
|
let typed_repositories: workspace_api::RepositoryListResponse =
|
||||||
|
serde_json::from_value(repositories.clone()).unwrap();
|
||||||
|
assert_eq!(typed_repositories.items[0].id, TEST_REPOSITORY_ID);
|
||||||
assert_eq!(repositories["items"][0]["id"], TEST_REPOSITORY_ID);
|
assert_eq!(repositories["items"][0]["id"], TEST_REPOSITORY_ID);
|
||||||
assert_eq!(repositories["items"][0]["kind"], "git");
|
assert_eq!(repositories["items"][0]["kind"], "git");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -24064,6 +24075,8 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let repository_detail = get_json(app.clone(), "/api/repositories/main").await;
|
let repository_detail = get_json(app.clone(), "/api/repositories/main").await;
|
||||||
|
let _: workspace_api::RepositoryDetailResponse =
|
||||||
|
serde_json::from_value(repository_detail.clone()).unwrap();
|
||||||
assert_eq!(repository_detail["item"]["id"], TEST_REPOSITORY_ID);
|
assert_eq!(repository_detail["item"]["id"], TEST_REPOSITORY_ID);
|
||||||
let scoped_repository_detail = get_json(
|
let scoped_repository_detail = get_json(
|
||||||
app.clone(),
|
app.clone(),
|
||||||
@@ -24073,6 +24086,8 @@ mod tests {
|
|||||||
assert_eq!(scoped_repository_detail["item"]["id"], TEST_REPOSITORY_ID);
|
assert_eq!(scoped_repository_detail["item"]["id"], TEST_REPOSITORY_ID);
|
||||||
|
|
||||||
let repository_log = get_json(app.clone(), "/api/repositories/main/log?limit=3").await;
|
let repository_log = get_json(app.clone(), "/api/repositories/main/log?limit=3").await;
|
||||||
|
let _: workspace_api::RepositoryLogResponse =
|
||||||
|
serde_json::from_value(repository_log.clone()).unwrap();
|
||||||
assert_eq!(repository_log["repository_id"], TEST_REPOSITORY_ID);
|
assert_eq!(repository_log["repository_id"], TEST_REPOSITORY_ID);
|
||||||
assert_eq!(repository_log["default_selector"], "HEAD");
|
assert_eq!(repository_log["default_selector"], "HEAD");
|
||||||
assert_eq!(repository_log["limit"], 3);
|
assert_eq!(repository_log["limit"], 3);
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ pub struct WorkspaceCreateRequest {
|
|||||||
pub repository: InitialRepositoryIntent,
|
pub repository: InitialRepositoryIntent,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct WorkspaceCreateResponse {
|
pub struct WorkspaceCreateResult {
|
||||||
pub workspace: WorkspaceRecord,
|
pub workspace: WorkspaceRecord,
|
||||||
pub repository: RepositoryRecord,
|
pub repository: RepositoryRecord,
|
||||||
pub config_revision: u64,
|
pub config_revision: u64,
|
||||||
@@ -81,7 +81,7 @@ impl WorkspaceCatalogService {
|
|||||||
&self,
|
&self,
|
||||||
request: WorkspaceCreateRequest,
|
request: WorkspaceCreateRequest,
|
||||||
owner_account_id: String,
|
owner_account_id: String,
|
||||||
) -> Result<WorkspaceCreateResponse> {
|
) -> Result<WorkspaceCreateResult> {
|
||||||
self.create_internal(request, owner_account_id, None)
|
self.create_internal(request, owner_account_id, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ impl WorkspaceCatalogService {
|
|||||||
request: WorkspaceCreateRequest,
|
request: WorkspaceCreateRequest,
|
||||||
owner_account_id: String,
|
owner_account_id: String,
|
||||||
requested_workspace_id: Option<String>,
|
requested_workspace_id: Option<String>,
|
||||||
) -> Result<WorkspaceCreateResponse> {
|
) -> Result<WorkspaceCreateResult> {
|
||||||
let operation_key = normalize_required(
|
let operation_key = normalize_required(
|
||||||
"operation_key",
|
"operation_key",
|
||||||
request.operation_key,
|
request.operation_key,
|
||||||
@@ -165,7 +165,7 @@ impl WorkspaceCatalogService {
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
},
|
},
|
||||||
})?;
|
})?;
|
||||||
Ok(WorkspaceCreateResponse {
|
Ok(WorkspaceCreateResult {
|
||||||
workspace: result.workspace,
|
workspace: result.workspace,
|
||||||
repository: result.repository,
|
repository: result.repository,
|
||||||
config_revision: result.config_revision,
|
config_revision: result.config_revision,
|
||||||
|
|||||||
Reference in New Issue
Block a user