chore: merge develop into hare/develop
This commit is contained in:
@@ -10,10 +10,6 @@ autobins = false
|
||||
name = "yoi-server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["typescript"]
|
||||
typescript = ["dep:ts-rs", "server-api/typescript"]
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
@@ -47,7 +43,7 @@ worker-runtime.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
ts-rs = { version = "12.0.1", optional = true }
|
||||
ts-rs = "12.0.1"
|
||||
url.workspace = true
|
||||
uuid = { workspace = true, features = ["v7"] }
|
||||
zeroize.workspace = true
|
||||
@@ -57,3 +53,4 @@ webauthn-rs = { workspace = true }
|
||||
tempfile.workspace = true
|
||||
tower = { workspace = true, features = ["util"] }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
webauthn-authenticator-rs = { version = "0.5.5", default-features = false, features = ["softpasskey"] }
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
print!("{}", yoi_workspace_server::ticket_api_typescript());
|
||||
}
|
||||
@@ -19,10 +19,10 @@ use crate::records::{
|
||||
ObjectiveQueryItem, ObjectiveQueryRequest, ObjectiveQueryResponse, ObjectiveResourceSummary,
|
||||
ObjectiveShowRequest, ObjectiveSummary, ProjectRecordList, QueryPage, TicketActionEligibility,
|
||||
TicketAssignmentPrincipalSummary, TicketAssignmentSummary, TicketDetail, TicketEventDetail,
|
||||
TicketEvidenceEvent, TicketEvidenceSummary, TicketListPageRequest, TicketMergeRequestSummary,
|
||||
TicketQueryItem, TicketQueryRequest, TicketQueryResponse, TicketRelationView,
|
||||
TicketEvidenceEvent, TicketEvidenceSummary, TicketListProjectionRequest,
|
||||
TicketMergeRequestSummary, TicketQueryItem, TicketQueryRequest, TicketQueryResponse,
|
||||
TicketRoleAssignmentSummary, TicketShowRequest, TicketSummary, TicketSummaryPage,
|
||||
summarize_body, truncate_body,
|
||||
summarize_body, ticket_relation_view_from_domain, truncate_body,
|
||||
};
|
||||
use crate::store::{
|
||||
ControlPlaneStore, MemoryDocumentRecord, MemoryStagingRecord, MemoryStagingResolutionRecord,
|
||||
@@ -48,7 +48,7 @@ impl<T> WorkspaceAuthority for T where T: ObjectiveAuthority + TicketAuthority +
|
||||
|
||||
pub trait TicketAuthority {
|
||||
fn list_tickets(&self, limit: usize) -> Result<ProjectRecordList<TicketSummary>>;
|
||||
fn list_ticket_page(&self, request: TicketListPageRequest) -> Result<TicketSummaryPage>;
|
||||
fn list_ticket_page(&self, request: TicketListProjectionRequest) -> Result<TicketSummaryPage>;
|
||||
fn query_tickets(&self, query: TicketQueryRequest) -> Result<TicketQueryResponse>;
|
||||
fn ticket(&self, id: &str) -> Result<TicketDetail>;
|
||||
fn show_ticket(&self, id: &str, query: TicketShowRequest) -> Result<TicketDetail>;
|
||||
@@ -86,14 +86,14 @@ pub trait MemoryAuthority {
|
||||
fn ensure_memory_document(&self) -> Result<MemoryDocument>;
|
||||
fn memory_document(&self) -> Result<MemoryDocument>;
|
||||
fn update_memory_document(&self, body_md: &str) -> Result<MemoryDocument>;
|
||||
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<MemoryStagingEntry>>;
|
||||
fn memory_staging_record(&self, candidate_id: &str) -> Result<MemoryStagingEntry>;
|
||||
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<InternalMemoryStagingEntry>>;
|
||||
fn memory_staging_record(&self, candidate_id: &str) -> Result<InternalMemoryStagingEntry>;
|
||||
fn upsert_memory_staging_record(
|
||||
&self,
|
||||
candidate_id: &str,
|
||||
raw_json: &str,
|
||||
source_path: Option<&str>,
|
||||
) -> Result<MemoryStagingEntry>;
|
||||
) -> Result<InternalMemoryStagingEntry>;
|
||||
fn close_memory_staging_record(
|
||||
&self,
|
||||
candidate_id: &str,
|
||||
@@ -114,7 +114,7 @@ pub struct MemoryDocument {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MemoryStagingEntry {
|
||||
pub struct InternalMemoryStagingEntry {
|
||||
pub candidate_id: String,
|
||||
pub raw_json: String,
|
||||
pub source_path: Option<String>,
|
||||
@@ -840,7 +840,24 @@ impl SqliteWorkspaceAuthority {
|
||||
let has_coder = role_assignments
|
||||
.iter()
|
||||
.any(|assignment| assignment.role == TicketAssignmentRole::Coder);
|
||||
let has_target = ticket.meta.repository_id.is_some() && ticket.meta.ref_selector.is_some();
|
||||
let targets = ticket
|
||||
.meta
|
||||
.targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
let repository = self
|
||||
.store
|
||||
.get_repository_by_key(&self.workspace_id, &target.repository_key)?
|
||||
.ok_or_else(|| Error::UnknownRepository(target.repository_key.clone()))?;
|
||||
Ok::<_, Error>(ticket::TicketTarget {
|
||||
repository_key: repository.repository_key,
|
||||
ref_selector: target.ref_selector.clone(),
|
||||
access: target.access,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let write_repository_key = ticket_write_target_repository_key(&targets);
|
||||
let has_target = write_repository_key.is_some();
|
||||
let has_blockers = !ticket.relations.blockers.is_empty();
|
||||
let mut queue_assignment_blockers = Vec::new();
|
||||
for ticket_id in &dependency_check.queue_tickets {
|
||||
@@ -932,22 +949,8 @@ impl SqliteWorkspaceAuthority {
|
||||
Err(MergeRequestError::NotFound) => None,
|
||||
Err(error) => return Err(Error::Store(error.to_string())),
|
||||
};
|
||||
let repository_key = ticket
|
||||
.meta
|
||||
.repository_id
|
||||
.as_deref()
|
||||
.map(|repository_id| {
|
||||
self.store
|
||||
.get_repository(&self.workspace_id, repository_id)?
|
||||
.map(|repository| repository.repository_key)
|
||||
.ok_or_else(|| Error::UnknownRepository(repository_id.to_string()))
|
||||
})
|
||||
.transpose()?;
|
||||
let evidence = ticket_evidence_summary(
|
||||
repository_key.as_deref(),
|
||||
&ticket.events,
|
||||
merge_request.as_ref(),
|
||||
);
|
||||
let evidence =
|
||||
ticket_evidence_summary(write_repository_key, &ticket.events, merge_request.as_ref());
|
||||
let item_revision = ticket
|
||||
.events
|
||||
.iter()
|
||||
@@ -966,7 +969,7 @@ impl SqliteWorkspaceAuthority {
|
||||
&ticket.meta.id,
|
||||
)?)
|
||||
.ok_or_else(|| Error::Store(format!("missing resource key for {}", ticket.meta.id)))?;
|
||||
let mut relations: TicketRelationView = ticket.relations.into();
|
||||
let mut relations = ticket_relation_view_from_domain(ticket.relations);
|
||||
for relation in &mut relations.outgoing {
|
||||
relation.target_resource_key = self.store.resource_key(
|
||||
&self.workspace_id,
|
||||
@@ -1000,8 +1003,7 @@ impl SqliteWorkspaceAuthority {
|
||||
item_revision,
|
||||
queued_by: ticket.meta.queued_by,
|
||||
queued_at: ticket.meta.queued_at,
|
||||
repository_key,
|
||||
ref_selector: ticket.meta.ref_selector,
|
||||
targets,
|
||||
risk_flags: ticket.meta.risk_flags,
|
||||
body,
|
||||
body_truncated,
|
||||
@@ -1073,7 +1075,7 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
|
||||
})
|
||||
}
|
||||
|
||||
fn list_ticket_page(&self, request: TicketListPageRequest) -> Result<TicketSummaryPage> {
|
||||
fn list_ticket_page(&self, request: TicketListProjectionRequest) -> Result<TicketSummaryPage> {
|
||||
let limit = request.limit.unwrap_or(30).clamp(1, 100);
|
||||
let mut states = request.states;
|
||||
states.sort();
|
||||
@@ -1587,7 +1589,7 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
|
||||
Ok(memory_document_from_record(record))
|
||||
}
|
||||
|
||||
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<MemoryStagingEntry>> {
|
||||
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<InternalMemoryStagingEntry>> {
|
||||
self.store
|
||||
.list_memory_staging_records(&self.workspace_id, limit)
|
||||
.map(|records| {
|
||||
@@ -1598,7 +1600,7 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
|
||||
})
|
||||
}
|
||||
|
||||
fn memory_staging_record(&self, candidate_id: &str) -> Result<MemoryStagingEntry> {
|
||||
fn memory_staging_record(&self, candidate_id: &str) -> Result<InternalMemoryStagingEntry> {
|
||||
validate_memory_candidate_id(candidate_id)?;
|
||||
self.store
|
||||
.get_memory_staging_record(&self.workspace_id, candidate_id)?
|
||||
@@ -1613,7 +1615,7 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
|
||||
candidate_id: &str,
|
||||
raw_json: &str,
|
||||
source_path: Option<&str>,
|
||||
) -> Result<MemoryStagingEntry> {
|
||||
) -> Result<InternalMemoryStagingEntry> {
|
||||
validate_memory_candidate_id(candidate_id)?;
|
||||
validate_json_object(raw_json, "raw_json")?;
|
||||
let imported_at = now_rfc3339();
|
||||
@@ -1837,7 +1839,7 @@ fn substantive_item_edit(event: &TicketEvent) -> bool {
|
||||
changes
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.any(|field| matches!(field, "title" | "body" | "target"))
|
||||
.any(|field| matches!(field, "title" | "body" | "target" | "targets"))
|
||||
}
|
||||
|
||||
fn event_timestamp(event: &TicketEvent) -> Option<DateTime<Utc>> {
|
||||
@@ -1848,15 +1850,26 @@ fn event_timestamp(event: &TicketEvent) -> Option<DateTime<Utc>> {
|
||||
.map(|value| value.with_timezone(&Utc))
|
||||
}
|
||||
|
||||
fn ticket_write_target_repository_key(targets: &[ticket::TicketTarget]) -> Option<&str> {
|
||||
let mut write_targets = targets
|
||||
.iter()
|
||||
.filter(|target| target.access == ticket::TicketTargetAccess::ReadWrite);
|
||||
let write_target = write_targets.next()?;
|
||||
write_targets
|
||||
.next()
|
||||
.is_none()
|
||||
.then_some(write_target.repository_key.as_str())
|
||||
}
|
||||
|
||||
fn ticket_evidence_summary(
|
||||
ticket_repository_id: Option<&str>,
|
||||
ticket_write_repository_key: Option<&str>,
|
||||
events: &[TicketEvent],
|
||||
merge_request: Option<&TicketMergeRequestSummary>,
|
||||
) -> TicketEvidenceSummary {
|
||||
let linked_merge_request = merge_request.filter(|request| {
|
||||
request.state == "open"
|
||||
&& ticket_repository_id
|
||||
.is_some_and(|repository_id| repository_id == request.repository_key)
|
||||
&& ticket_write_repository_key
|
||||
.is_some_and(|repository_key| repository_key == request.repository_key)
|
||||
});
|
||||
let has_merge_request = linked_merge_request.is_some();
|
||||
let has_current_subject_ref = linked_merge_request.is_some_and(|request| {
|
||||
@@ -1906,8 +1919,8 @@ fn ticket_evidence_summary(
|
||||
None => missing.push("merge_request".to_string()),
|
||||
Some(request) if request.state != "open" => missing.push("open_merge_request".to_string()),
|
||||
Some(request)
|
||||
if ticket_repository_id
|
||||
.is_none_or(|repository_id| repository_id != request.repository_key) =>
|
||||
if ticket_write_repository_key
|
||||
.is_none_or(|repository_key| repository_key != request.repository_key) =>
|
||||
{
|
||||
missing.push("merge_request_repository".to_string())
|
||||
}
|
||||
@@ -2634,8 +2647,8 @@ fn memory_document_from_record(record: MemoryDocumentRecord) -> MemoryDocument {
|
||||
}
|
||||
}
|
||||
|
||||
fn memory_staging_from_record(record: MemoryStagingRecord) -> MemoryStagingEntry {
|
||||
MemoryStagingEntry {
|
||||
fn memory_staging_from_record(record: MemoryStagingRecord) -> InternalMemoryStagingEntry {
|
||||
InternalMemoryStagingEntry {
|
||||
candidate_id: record.candidate_id,
|
||||
raw_json: record.raw_json,
|
||||
source_path: record.source_path,
|
||||
@@ -2865,6 +2878,54 @@ mod tests {
|
||||
assert_eq!(moved.review_submitted_at, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_target_projection_requires_exactly_one_write_target() {
|
||||
let target = |repository_key: &str, access| ticket::TicketTarget {
|
||||
repository_key: repository_key.to_string(),
|
||||
ref_selector: Some("develop".to_string()),
|
||||
access,
|
||||
};
|
||||
|
||||
assert_eq!(ticket_write_target_repository_key(&[]), None);
|
||||
assert_eq!(
|
||||
ticket_write_target_repository_key(&[target(
|
||||
"docs",
|
||||
ticket::TicketTargetAccess::ReadOnly,
|
||||
)]),
|
||||
None,
|
||||
);
|
||||
|
||||
let one_write = [
|
||||
target("main", ticket::TicketTargetAccess::ReadWrite),
|
||||
target("docs", ticket::TicketTargetAccess::ReadOnly),
|
||||
];
|
||||
assert_eq!(ticket_write_target_repository_key(&one_write), Some("main"),);
|
||||
|
||||
let two_writes = [
|
||||
target("main", ticket::TicketTargetAccess::ReadWrite),
|
||||
target("docs", ticket::TicketTargetAccess::ReadWrite),
|
||||
];
|
||||
assert_eq!(ticket_write_target_repository_key(&two_writes), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_evidence_is_tied_only_to_the_write_target() {
|
||||
let read_only_request = merge_request_summary(
|
||||
reviewed_merge_request(ReviewDecision::Approve, false),
|
||||
"docs".to_string(),
|
||||
Some("commit-1".to_string()),
|
||||
);
|
||||
let evidence = ticket_evidence_summary(Some("main"), &[], Some(&read_only_request));
|
||||
|
||||
assert!(!evidence.has_merge_request);
|
||||
assert!(!evidence.complete_for_integration);
|
||||
assert!(
|
||||
evidence
|
||||
.missing
|
||||
.contains(&"merge_request_repository".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_readiness_requires_current_unrevoked_approval_without_a_report() {
|
||||
let approved = merge_request_summary(
|
||||
@@ -3013,7 +3074,7 @@ mod tests {
|
||||
let stale_events = vec![ticket_event(
|
||||
"item_edit",
|
||||
"2026-01-01T00:05:00Z",
|
||||
Some("target"),
|
||||
Some("targets"),
|
||||
)];
|
||||
let stale = ticket_evidence_summary(Some("main"), &stale_events, Some(&approved_summary));
|
||||
assert!(ticket_attention_matches(
|
||||
@@ -3330,7 +3391,7 @@ VALUES ('workspace-test', 'ticket', 4);
|
||||
assert_eq!(incoming_relation.items.len(), 1);
|
||||
assert_eq!(incoming_relation.items[0].id, "00000000001J5");
|
||||
let summary_page = authority
|
||||
.list_ticket_page(TicketListPageRequest {
|
||||
.list_ticket_page(TicketListProjectionRequest {
|
||||
states: vec!["planning".to_string(), "ready".to_string()],
|
||||
limit: Some(1),
|
||||
cursor: None,
|
||||
@@ -3338,7 +3399,7 @@ VALUES ('workspace-test', 'ticket', 4);
|
||||
.unwrap();
|
||||
assert_eq!(summary_page.items.len(), 1);
|
||||
assert!(summary_page.page.has_more);
|
||||
let mismatched_summary_cursor = authority.list_ticket_page(TicketListPageRequest {
|
||||
let mismatched_summary_cursor = authority.list_ticket_page(TicketListProjectionRequest {
|
||||
states: vec!["done".to_string()],
|
||||
limit: Some(1),
|
||||
cursor: summary_page.page.next_cursor,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,6 @@ pub mod observation;
|
||||
pub mod profile_settings;
|
||||
pub mod prompt_settings;
|
||||
pub mod records;
|
||||
#[cfg(feature = "typescript")]
|
||||
pub use records::ticket_api_typescript;
|
||||
pub mod repositories;
|
||||
pub mod repository_access;
|
||||
pub mod repository_source;
|
||||
@@ -38,7 +36,7 @@ pub mod workspace_signing_identity;
|
||||
mod workspace_subscription;
|
||||
|
||||
pub use authority::{
|
||||
MemoryAuthority, MemoryDocument, MemoryStagingEntry, MemoryStagingResolution,
|
||||
InternalMemoryStagingEntry, MemoryAuthority, MemoryDocument, MemoryStagingResolution,
|
||||
ObjectiveAuthority, SqliteWorkspaceAuthority, TicketAuthority, TicketMergeRevisionSource,
|
||||
WorkspaceAuthority,
|
||||
};
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use server_api::{
|
||||
ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkedTicketSummary, ObjectiveResourceSummary,
|
||||
ObjectiveSummary, QueryPage,
|
||||
InvalidProjectRecord, MergeRequestListItem, MergeRequestListResponse,
|
||||
MergeRequestRefDiagnostic, ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkSummary,
|
||||
ObjectiveLinkedTicketSummary, ObjectiveQueryItem, ObjectiveQueryRequest,
|
||||
ObjectiveQueryResponse, ObjectiveResourceSummary, ObjectiveShowRequest, ObjectiveSummary,
|
||||
QueryPage, TicketActionEligibility,
|
||||
TicketAssignmentPrincipal as TicketAssignmentPrincipalSummary, TicketAssignmentSummary,
|
||||
TicketDetail, TicketDetailDerivedRelation as DerivedTicketRelation,
|
||||
TicketDetailRelation as TicketRelation, TicketDetailRelationBlocker as TicketRelationBlocker,
|
||||
TicketDetailRelationNotice as TicketRelationNotice,
|
||||
TicketDetailRelationView as TicketRelationView, TicketEventDetail, TicketEvidenceEvent,
|
||||
TicketEvidenceSummary, TicketListItemSummary as TicketSummary, TicketListResponse,
|
||||
TicketMergeRequestSummary, TicketQueryItem, TicketQueryRequest, TicketQueryResponse,
|
||||
TicketRoleAssignmentSummary, TicketShowRequest,
|
||||
};
|
||||
|
||||
const SUMMARY_BODY_LIMIT: usize = 240;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProjectRecordList<T> {
|
||||
pub items: Vec<T>,
|
||||
pub invalid_records: Vec<InvalidProjectRecord>,
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketSummaryPage {
|
||||
pub items: Vec<TicketSummary>,
|
||||
pub page: QueryPage,
|
||||
@@ -21,515 +31,64 @@ pub struct TicketSummaryPage {
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct InvalidProjectRecord {
|
||||
pub label: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketSummary {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub priority: String,
|
||||
pub updated_at: Option<String>,
|
||||
pub queued_by: Option<String>,
|
||||
pub queued_at: Option<String>,
|
||||
pub workspace_action_priority: String,
|
||||
pub record_source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct TicketListPageRequest {
|
||||
#[serde(default)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct TicketListProjectionRequest {
|
||||
pub states: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub limit: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketListResponse {
|
||||
pub workspace_id: String,
|
||||
pub limit: usize,
|
||||
pub items: Vec<TicketSummary>,
|
||||
pub page: QueryPage,
|
||||
pub invalid_records: Vec<InvalidProjectRecord>,
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketDetail {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub readiness: Option<String>,
|
||||
pub priority: String,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub item_revision: String,
|
||||
pub queued_by: Option<String>,
|
||||
pub queued_at: Option<String>,
|
||||
pub repository_key: Option<String>,
|
||||
pub ref_selector: Option<String>,
|
||||
pub risk_flags: Vec<String>,
|
||||
pub body: String,
|
||||
pub body_truncated: bool,
|
||||
pub event_count: usize,
|
||||
pub events: Vec<TicketEventDetail>,
|
||||
pub event_page: QueryPage,
|
||||
pub artifact_count: usize,
|
||||
pub artifacts: Vec<String>,
|
||||
pub relations: TicketRelationView,
|
||||
pub linked_objectives: Vec<ObjectiveLinkSummary>,
|
||||
pub implementation_reports: Vec<TicketEvidenceEvent>,
|
||||
pub assignments: Vec<TicketRoleAssignmentSummary>,
|
||||
pub current_coder: Option<TicketAssignmentSummary>,
|
||||
pub assignment_diagnostics: Vec<String>,
|
||||
pub action_eligibility: TicketActionEligibility,
|
||||
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||
pub evidence: TicketEvidenceSummary,
|
||||
pub resolution: Option<String>,
|
||||
pub record_source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketEventDetail {
|
||||
pub sequence: usize,
|
||||
pub event_ref: String,
|
||||
pub kind: String,
|
||||
pub author: Option<String>,
|
||||
pub at: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub from: Option<String>,
|
||||
pub to: Option<String>,
|
||||
pub reason: Option<String>,
|
||||
pub state_field: Option<String>,
|
||||
pub heading: Option<String>,
|
||||
pub body: Option<String>,
|
||||
pub attributes: std::collections::BTreeMap<String, String>,
|
||||
pub references: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelation {
|
||||
pub ticket_id: String,
|
||||
pub kind: String,
|
||||
pub target: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_resource_key: Option<String>,
|
||||
pub note: Option<String>,
|
||||
pub author: String,
|
||||
pub at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct DerivedTicketRelation {
|
||||
pub source_ticket: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub source_resource_key: Option<String>,
|
||||
pub inverse_kind: String,
|
||||
pub forward_kind: String,
|
||||
pub note: Option<String>,
|
||||
pub author: String,
|
||||
pub at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelationBlocker {
|
||||
pub blocking_ticket: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub blocking_resource_key: Option<String>,
|
||||
pub reason_kind: String,
|
||||
pub relation_kind: String,
|
||||
pub note: Option<String>,
|
||||
pub blocking_state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelationNotice {
|
||||
pub related_ticket: String,
|
||||
pub kind: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelationView {
|
||||
pub outgoing: Vec<TicketRelation>,
|
||||
pub incoming: Vec<DerivedTicketRelation>,
|
||||
pub blockers: Vec<TicketRelationBlocker>,
|
||||
pub notices: Vec<TicketRelationNotice>,
|
||||
}
|
||||
|
||||
impl From<ticket::TicketRelationView> for TicketRelationView {
|
||||
fn from(value: ticket::TicketRelationView) -> Self {
|
||||
Self {
|
||||
outgoing: value
|
||||
.outgoing
|
||||
.into_iter()
|
||||
.map(|relation| TicketRelation {
|
||||
ticket_id: relation.ticket_id,
|
||||
kind: relation.kind.as_str().to_string(),
|
||||
target: relation.target,
|
||||
target_resource_key: None,
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect(),
|
||||
incoming: value
|
||||
.incoming
|
||||
.into_iter()
|
||||
.map(|relation| DerivedTicketRelation {
|
||||
source_ticket: relation.source_ticket,
|
||||
source_resource_key: None,
|
||||
inverse_kind: relation.inverse_kind,
|
||||
forward_kind: relation.forward_kind.as_str().to_string(),
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect(),
|
||||
blockers: value
|
||||
.blockers
|
||||
.into_iter()
|
||||
.map(|blocker| TicketRelationBlocker {
|
||||
blocking_ticket: blocker.blocking_ticket,
|
||||
blocking_resource_key: None,
|
||||
reason_kind: blocker.reason_kind,
|
||||
relation_kind: blocker.relation_kind.as_str().to_string(),
|
||||
note: blocker.note,
|
||||
blocking_state: blocker.blocking_state.as_str().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
notices: value
|
||||
.notices
|
||||
.into_iter()
|
||||
.map(|notice| TicketRelationNotice {
|
||||
related_ticket: notice.related_ticket,
|
||||
kind: notice.kind.as_str().to_string(),
|
||||
message: notice.message,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct ObjectiveLinkSummary {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketEvidenceEvent {
|
||||
pub event_ref: String,
|
||||
pub sequence: usize,
|
||||
pub kind: String,
|
||||
pub at: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub excerpt: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketAssignmentSummary {
|
||||
pub assignment_id: String,
|
||||
pub runtime_id: String,
|
||||
pub worker_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub worker_resource_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRoleAssignmentSummary {
|
||||
pub assignment_id: String,
|
||||
pub role: String,
|
||||
pub principal: TicketAssignmentPrincipalSummary,
|
||||
pub assigned_by: String,
|
||||
pub assigned_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "typescript", ts(tag = "kind", rename_all = "snake_case"))]
|
||||
pub enum TicketAssignmentPrincipalSummary {
|
||||
User {
|
||||
account_id: String,
|
||||
},
|
||||
Worker {
|
||||
runtime_id: String,
|
||||
worker_id: String,
|
||||
},
|
||||
WorkspaceAgent {
|
||||
agent_key: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketActionEligibility {
|
||||
pub can_assign_orchestrator: bool,
|
||||
pub can_unassign_orchestrator: bool,
|
||||
pub can_queue: bool,
|
||||
pub can_start_manual_coder: bool,
|
||||
pub queue_tickets: Vec<String>,
|
||||
pub blockers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketMergeRequestSummary {
|
||||
pub merge_request_id: String,
|
||||
pub repository_key: String,
|
||||
pub state: String,
|
||||
pub review_status: String,
|
||||
pub selector_from: Option<String>,
|
||||
pub selector_to: String,
|
||||
pub updated_at: String,
|
||||
pub current_subject_ref: Option<String>,
|
||||
pub review_subject_ref: Option<String>,
|
||||
pub review_requested_at: Option<String>,
|
||||
pub review_submitted_at: Option<String>,
|
||||
pub review_excerpt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct MergeRequestRefDiagnostic {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct MergeRequestListItem {
|
||||
pub summary: TicketMergeRequestSummary,
|
||||
pub ticket_ids: Vec<String>,
|
||||
pub thread_event_count: usize,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub ref_diagnostics: Vec<MergeRequestRefDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct MergeRequestListResponse {
|
||||
pub items: Vec<MergeRequestListItem>,
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketEvidenceSummary {
|
||||
pub has_merge_request: bool,
|
||||
pub has_current_subject_ref: bool,
|
||||
pub has_review_request: bool,
|
||||
pub has_commit: bool,
|
||||
pub review_status: Option<String>,
|
||||
pub approved_current_subject: bool,
|
||||
pub review_after_rescope: bool,
|
||||
pub unresolved_request_changes: bool,
|
||||
pub complete_for_integration: bool,
|
||||
pub missing: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketQueryRequest {
|
||||
pub query: Option<String>,
|
||||
#[serde(default)]
|
||||
pub states: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub event_kinds: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub evidence: Vec<String>,
|
||||
pub review_status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub attention: Vec<String>,
|
||||
pub related_ticket_id: Option<String>,
|
||||
pub relation_kind: Option<String>,
|
||||
pub linked_objective_id: Option<String>,
|
||||
pub updated_after: Option<String>,
|
||||
pub updated_before: Option<String>,
|
||||
pub sort: Option<String>,
|
||||
pub limit: Option<usize>,
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketQueryItem {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub readiness: Option<String>,
|
||||
pub priority: String,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub item_revision: String,
|
||||
pub workspace_action_priority: String,
|
||||
pub matched_fields: Vec<String>,
|
||||
pub snippet: Option<String>,
|
||||
pub matching_event: Option<TicketEvidenceEvent>,
|
||||
pub linked_objective_ids: Vec<String>,
|
||||
#[ts(skip)]
|
||||
pub linked_objective_keys: Vec<String>,
|
||||
pub relation_count: usize,
|
||||
pub blocker_count: usize,
|
||||
pub unresolved_blocker_count: usize,
|
||||
pub unresolved_review_count: usize,
|
||||
pub evidence: TicketEvidenceSummary,
|
||||
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||
#[ts(skip)]
|
||||
pub current_coder: Option<TicketAssignmentSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketQueryResponse {
|
||||
pub items: Vec<TicketQueryItem>,
|
||||
pub page: QueryPage,
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketShowRequest {
|
||||
pub event_limit: Option<usize>,
|
||||
pub event_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct ObjectiveQueryRequest {
|
||||
pub query: Option<String>,
|
||||
#[serde(default)]
|
||||
pub states: Vec<String>,
|
||||
pub linked_ticket_id: Option<String>,
|
||||
pub updated_after: Option<String>,
|
||||
pub updated_before: Option<String>,
|
||||
pub sort: Option<String>,
|
||||
pub limit: Option<usize>,
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ObjectiveQueryItem {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub matched_fields: Vec<String>,
|
||||
pub snippet: Option<String>,
|
||||
pub linked_ticket_count: usize,
|
||||
pub linked_tickets: Vec<String>,
|
||||
pub linked_ticket_keys: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ObjectiveQueryResponse {
|
||||
pub items: Vec<ObjectiveQueryItem>,
|
||||
pub page: QueryPage,
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct ObjectiveShowRequest {
|
||||
pub event_limit: Option<usize>,
|
||||
pub event_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "typescript")]
|
||||
pub fn ticket_api_typescript() -> String {
|
||||
use ts_rs::TS;
|
||||
|
||||
let config = ts_rs::Config::default();
|
||||
let declarations = [
|
||||
InvalidProjectRecord::decl(&config),
|
||||
TicketSummary::decl(&config),
|
||||
TicketListResponse::decl(&config),
|
||||
QueryPage::decl(&config),
|
||||
TicketEventDetail::decl(&config),
|
||||
ObjectiveLinkSummary::decl(&config),
|
||||
TicketEvidenceEvent::decl(&config),
|
||||
TicketAssignmentSummary::decl(&config),
|
||||
TicketRoleAssignmentSummary::decl(&config),
|
||||
TicketAssignmentPrincipalSummary::decl(&config),
|
||||
TicketActionEligibility::decl(&config),
|
||||
TicketMergeRequestSummary::decl(&config),
|
||||
MergeRequestRefDiagnostic::decl(&config),
|
||||
MergeRequestListItem::decl(&config),
|
||||
MergeRequestListResponse::decl(&config),
|
||||
TicketEvidenceSummary::decl(&config),
|
||||
TicketQueryRequest::decl(&config),
|
||||
TicketQueryItem::decl(&config),
|
||||
TicketQueryResponse::decl(&config),
|
||||
TicketShowRequest::decl(&config),
|
||||
TicketRelation::decl(&config),
|
||||
DerivedTicketRelation::decl(&config),
|
||||
TicketRelationBlocker::decl(&config),
|
||||
TicketRelationNotice::decl(&config),
|
||||
TicketRelationView::decl(&config),
|
||||
TicketDetail::decl(&config),
|
||||
];
|
||||
format!(
|
||||
"// Generated from yoi-workspace-server. Do not edit by hand.\n// Regenerate: cargo run -q -p yoi-workspace-server --features typescript --example generate_ticket_api_types > web/workspace/src/lib/generated/ticket-api.ts\n\n{}\n",
|
||||
declarations
|
||||
pub(crate) fn ticket_relation_view_from_domain(
|
||||
value: ticket::TicketRelationView,
|
||||
) -> TicketRelationView {
|
||||
TicketRelationView {
|
||||
outgoing: value
|
||||
.outgoing
|
||||
.into_iter()
|
||||
.map(|declaration| format!("export {declaration}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n")
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "typescript"))]
|
||||
mod typescript_tests {
|
||||
#[test]
|
||||
fn generated_ticket_api_contract_is_current() {
|
||||
let expected = super::ticket_api_typescript();
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../web/workspace/src/lib/generated/ticket-api.ts");
|
||||
let actual = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
|
||||
assert_eq!(
|
||||
normalize(&actual),
|
||||
normalize(&expected),
|
||||
"regenerate Ticket API TypeScript types with `cargo run -q -p yoi-workspace-server --features typescript --example generate_ticket_api_types > web/workspace/src/lib/generated/ticket-api.ts` and format the generated file",
|
||||
);
|
||||
}
|
||||
|
||||
fn normalize(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.filter_map(|character| match character {
|
||||
character if character.is_whitespace() => None,
|
||||
',' => Some(';'),
|
||||
character => Some(character),
|
||||
.map(|relation| TicketRelation {
|
||||
ticket_id: relation.ticket_id,
|
||||
kind: relation.kind.as_str().to_string(),
|
||||
target: relation.target,
|
||||
target_resource_key: None,
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect::<String>()
|
||||
.replace(";}", "}")
|
||||
.collect(),
|
||||
incoming: value
|
||||
.incoming
|
||||
.into_iter()
|
||||
.map(|relation| DerivedTicketRelation {
|
||||
source_ticket: relation.source_ticket,
|
||||
source_resource_key: None,
|
||||
inverse_kind: relation.inverse_kind,
|
||||
forward_kind: relation.forward_kind.as_str().to_string(),
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect(),
|
||||
blockers: value
|
||||
.blockers
|
||||
.into_iter()
|
||||
.map(|blocker| TicketRelationBlocker {
|
||||
blocking_ticket: blocker.blocking_ticket,
|
||||
blocking_resource_key: None,
|
||||
reason_kind: blocker.reason_kind,
|
||||
relation_kind: blocker.relation_kind.as_str().to_string(),
|
||||
note: blocker.note,
|
||||
blocking_state: blocker.blocking_state.as_str().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
notices: value
|
||||
.notices
|
||||
.into_iter()
|
||||
.map(|notice| TicketRelationNotice {
|
||||
related_ticket: notice.related_ticket,
|
||||
kind: notice.kind.as_str().to_string(),
|
||||
message: notice.message,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8479
-3255
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -56,6 +56,7 @@ const WORKSPACE_DELETION_PURGE_TABLES: &[&str] = &[
|
||||
"typed_ticket_raw_frontmatter",
|
||||
"typed_ticket_relations",
|
||||
"typed_ticket_risk_flags",
|
||||
"typed_ticket_targets",
|
||||
"typed_tickets",
|
||||
"workdir_create_credential_candidates",
|
||||
"workdir_create_credential_revision_retentions",
|
||||
|
||||
Reference in New Issue
Block a user