feat: add bounded Ticket and Objective read APIs
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,7 @@ pub struct TicketDetail {
|
|||||||
pub priority: String,
|
pub priority: String,
|
||||||
pub created_at: Option<String>,
|
pub created_at: Option<String>,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
|
pub item_revision: String,
|
||||||
pub queued_by: Option<String>,
|
pub queued_by: Option<String>,
|
||||||
pub queued_at: Option<String>,
|
pub queued_at: Option<String>,
|
||||||
pub assignee: Option<String>,
|
pub assignee: Option<String>,
|
||||||
@@ -62,9 +63,14 @@ pub struct TicketDetail {
|
|||||||
pub body_truncated: bool,
|
pub body_truncated: bool,
|
||||||
pub event_count: usize,
|
pub event_count: usize,
|
||||||
pub events: Vec<TicketEventDetail>,
|
pub events: Vec<TicketEventDetail>,
|
||||||
|
pub event_page: QueryPage,
|
||||||
pub artifact_count: usize,
|
pub artifact_count: usize,
|
||||||
pub artifacts: Vec<String>,
|
pub artifacts: Vec<String>,
|
||||||
pub relations: TicketRelationView,
|
pub relations: TicketRelationView,
|
||||||
|
pub linked_objectives: Vec<ObjectiveLinkSummary>,
|
||||||
|
pub implementation_reports: Vec<TicketEvidenceEvent>,
|
||||||
|
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||||
|
pub evidence: TicketEvidenceSummary,
|
||||||
pub resolution: Option<String>,
|
pub resolution: Option<String>,
|
||||||
pub record_source: String,
|
pub record_source: String,
|
||||||
}
|
}
|
||||||
@@ -73,6 +79,7 @@ pub struct TicketDetail {
|
|||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
pub struct TicketEventDetail {
|
pub struct TicketEventDetail {
|
||||||
pub sequence: usize,
|
pub sequence: usize,
|
||||||
|
pub event_ref: String,
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
pub at: Option<String>,
|
pub at: Option<String>,
|
||||||
@@ -83,6 +90,8 @@ pub struct TicketEventDetail {
|
|||||||
pub state_field: Option<String>,
|
pub state_field: Option<String>,
|
||||||
pub heading: Option<String>,
|
pub heading: Option<String>,
|
||||||
pub body: Option<String>,
|
pub body: Option<String>,
|
||||||
|
pub attributes: std::collections::BTreeMap<String, String>,
|
||||||
|
pub references: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@@ -185,6 +194,170 @@ impl From<ticket::TicketRelationView> for TicketRelationView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub struct QueryPage {
|
||||||
|
pub limit: usize,
|
||||||
|
pub returned: usize,
|
||||||
|
pub has_more: bool,
|
||||||
|
pub next_cursor: Option<String>,
|
||||||
|
pub sort: String,
|
||||||
|
pub source_limit: Option<usize>,
|
||||||
|
pub source_truncated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub struct ObjectiveLinkSummary {
|
||||||
|
pub id: 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 TicketMergeRequestSummary {
|
||||||
|
pub merge_request_id: String,
|
||||||
|
pub state: String,
|
||||||
|
pub review_status: String,
|
||||||
|
pub revision_id: String,
|
||||||
|
pub base_commit: String,
|
||||||
|
pub head_commit: String,
|
||||||
|
pub changed_paths: Vec<String>,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub review_submitted_at: Option<String>,
|
||||||
|
pub review_excerpt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub struct TicketEvidenceSummary {
|
||||||
|
pub has_implementation_report: bool,
|
||||||
|
pub implementation_report_after_rescope: bool,
|
||||||
|
pub has_merge_request: bool,
|
||||||
|
pub has_commit: bool,
|
||||||
|
pub review_status: Option<String>,
|
||||||
|
pub approved: 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 text: 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 title: String,
|
||||||
|
pub state: String,
|
||||||
|
pub priority: String,
|
||||||
|
pub updated_at: Option<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>,
|
||||||
|
pub relation_count: usize,
|
||||||
|
pub blocker_count: usize,
|
||||||
|
pub evidence: TicketEvidenceSummary,
|
||||||
|
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 text: 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 title: String,
|
||||||
|
pub state: 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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub struct ObjectiveEventDetail {
|
||||||
|
pub event_ref: String,
|
||||||
|
pub kind: String,
|
||||||
|
pub body: Option<String>,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct ObjectiveSummary {
|
pub struct ObjectiveSummary {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -201,12 +374,15 @@ pub struct ObjectiveDetail {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
|
pub revision: String,
|
||||||
pub created_at: Option<String>,
|
pub created_at: Option<String>,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
pub linked_tickets: Vec<String>,
|
pub linked_tickets: Vec<String>,
|
||||||
pub resources: Vec<ObjectiveResourceSummary>,
|
pub resources: Vec<ObjectiveResourceSummary>,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub body_truncated: bool,
|
pub body_truncated: bool,
|
||||||
|
pub events: Vec<ObjectiveEventDetail>,
|
||||||
|
pub event_page: QueryPage,
|
||||||
pub record_source: String,
|
pub record_source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +403,16 @@ pub fn ticket_api_typescript() -> String {
|
|||||||
InvalidProjectRecord::decl(&config),
|
InvalidProjectRecord::decl(&config),
|
||||||
TicketSummary::decl(&config),
|
TicketSummary::decl(&config),
|
||||||
TicketListResponse::decl(&config),
|
TicketListResponse::decl(&config),
|
||||||
|
QueryPage::decl(&config),
|
||||||
TicketEventDetail::decl(&config),
|
TicketEventDetail::decl(&config),
|
||||||
|
ObjectiveLinkSummary::decl(&config),
|
||||||
|
TicketEvidenceEvent::decl(&config),
|
||||||
|
TicketMergeRequestSummary::decl(&config),
|
||||||
|
TicketEvidenceSummary::decl(&config),
|
||||||
|
TicketQueryRequest::decl(&config),
|
||||||
|
TicketQueryItem::decl(&config),
|
||||||
|
TicketQueryResponse::decl(&config),
|
||||||
|
TicketShowRequest::decl(&config),
|
||||||
TicketRelation::decl(&config),
|
TicketRelation::decl(&config),
|
||||||
DerivedTicketRelation::decl(&config),
|
DerivedTicketRelation::decl(&config),
|
||||||
TicketRelationBlocker::decl(&config),
|
TicketRelationBlocker::decl(&config),
|
||||||
|
|||||||
@@ -92,7 +92,10 @@ use crate::observation::{
|
|||||||
RuntimeObservationSource, RuntimeObservationSourceConfig,
|
RuntimeObservationSource, RuntimeObservationSourceConfig,
|
||||||
};
|
};
|
||||||
use crate::profile_settings::UpdateWorkspaceMetadataRequest;
|
use crate::profile_settings::UpdateWorkspaceMetadataRequest;
|
||||||
use crate::records::{ObjectiveDetail, ProjectRecordList, TicketDetail};
|
use crate::records::{
|
||||||
|
ObjectiveDetail, ObjectiveQueryRequest, ObjectiveQueryResponse, ObjectiveShowRequest,
|
||||||
|
ProjectRecordList, TicketDetail, TicketQueryRequest, TicketQueryResponse, TicketShowRequest,
|
||||||
|
};
|
||||||
use crate::repositories::{
|
use crate::repositories::{
|
||||||
ConfiguredRepository, MergeTargetObservation, RepositoryListProjection, RepositoryLogRead,
|
ConfiguredRepository, MergeTargetObservation, RepositoryListProjection, RepositoryLogRead,
|
||||||
RepositoryLookupError, RepositoryRegistryReader, RepositorySummary,
|
RepositoryLookupError, RepositoryRegistryReader, RepositorySummary,
|
||||||
@@ -1232,6 +1235,10 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
|||||||
"/api/w/{workspace_id}/tickets",
|
"/api/w/{workspace_id}/tickets",
|
||||||
get(scoped_list_tickets).post(scoped_create_ticket_record),
|
get(scoped_list_tickets).post(scoped_create_ticket_record),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/w/{workspace_id}/tickets/query",
|
||||||
|
post(scoped_query_tickets),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/w/{workspace_id}/memory",
|
"/api/w/{workspace_id}/memory",
|
||||||
get(scoped_get_memory_document),
|
get(scoped_get_memory_document),
|
||||||
@@ -1368,6 +1375,10 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
|||||||
"/api/w/{workspace_id}/tickets/{id}",
|
"/api/w/{workspace_id}/tickets/{id}",
|
||||||
get(scoped_get_ticket).patch(scoped_edit_ticket_item),
|
get(scoped_get_ticket).patch(scoped_edit_ticket_item),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/w/{workspace_id}/tickets/{id}/show",
|
||||||
|
post(scoped_show_ticket),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/w/{workspace_id}/tickets/{id}/assignment",
|
"/api/w/{workspace_id}/tickets/{id}/assignment",
|
||||||
get(scoped_get_ticket_worker_assignment)
|
get(scoped_get_ticket_worker_assignment)
|
||||||
@@ -1399,11 +1410,19 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
|||||||
"/api/w/{workspace_id}/objectives",
|
"/api/w/{workspace_id}/objectives",
|
||||||
get(scoped_list_objectives).post(scoped_create_objective),
|
get(scoped_list_objectives).post(scoped_create_objective),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/w/{workspace_id}/objectives/query",
|
||||||
|
post(scoped_query_objectives),
|
||||||
|
)
|
||||||
.route("/api/objectives/{id}", get(get_objective))
|
.route("/api/objectives/{id}", get(get_objective))
|
||||||
.route(
|
.route(
|
||||||
"/api/w/{workspace_id}/objectives/{objective_id}",
|
"/api/w/{workspace_id}/objectives/{objective_id}",
|
||||||
get(scoped_get_objective).patch(scoped_edit_objective),
|
get(scoped_get_objective).patch(scoped_edit_objective),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/w/{workspace_id}/objectives/{objective_id}/show",
|
||||||
|
post(scoped_show_objective),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/w/{workspace_id}/objectives/{objective_id}/state",
|
"/api/w/{workspace_id}/objectives/{objective_id}/state",
|
||||||
post(scoped_set_objective_state),
|
post(scoped_set_objective_state),
|
||||||
@@ -2643,6 +2662,24 @@ async fn scoped_get_ticket(
|
|||||||
get_ticket(State(api), AxumPath(path.id)).await
|
get_ticket(State(api), AxumPath(path.id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn scoped_query_tickets(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
|
Json(query): Json<TicketQueryRequest>,
|
||||||
|
) -> ApiResult<Json<TicketQueryResponse>> {
|
||||||
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
|
Ok(Json(api.authority.query_tickets(query)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn scoped_show_ticket(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
AxumPath(path): AxumPath<ScopedRecordPath>,
|
||||||
|
Json(query): Json<TicketShowRequest>,
|
||||||
|
) -> ApiResult<Json<TicketDetail>> {
|
||||||
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
|
Ok(Json(api.authority.show_ticket(&path.id, query)?))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
struct TicketWorkerAssignmentResponse {
|
struct TicketWorkerAssignmentResponse {
|
||||||
workspace_id: String,
|
workspace_id: String,
|
||||||
@@ -5581,6 +5618,26 @@ async fn scoped_get_objective(
|
|||||||
get_objective(State(api), AxumPath(path.objective_id)).await
|
get_objective(State(api), AxumPath(path.objective_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn scoped_query_objectives(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
|
Json(query): Json<ObjectiveQueryRequest>,
|
||||||
|
) -> ApiResult<Json<ObjectiveQueryResponse>> {
|
||||||
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
|
Ok(Json(api.authority.query_objectives(query)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn scoped_show_objective(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
AxumPath(path): AxumPath<ScopedObjectivePath>,
|
||||||
|
Json(query): Json<ObjectiveShowRequest>,
|
||||||
|
) -> ApiResult<Json<ObjectiveDetail>> {
|
||||||
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
|
Ok(Json(
|
||||||
|
api.authority.show_objective(&path.objective_id, query)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
async fn scoped_create_objective(
|
async fn scoped_create_objective(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
@@ -17703,10 +17760,64 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
assert_eq!(scoped_objective["id"], "00000000001J3");
|
assert_eq!(scoped_objective["id"], "00000000001J3");
|
||||||
assert_eq!(scoped_objective["record_source"], "workspace-sqlite");
|
assert_eq!(scoped_objective["record_source"], "workspace-sqlite");
|
||||||
|
assert_eq!(
|
||||||
|
scoped_objective["revision"].as_str().unwrap().is_empty(),
|
||||||
|
false
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scoped_objective["resources"][0]["path"],
|
scoped_objective["resources"][0]["path"],
|
||||||
"memory-architecture-overview.md"
|
"memory-architecture-overview.md"
|
||||||
);
|
);
|
||||||
|
let queried_tickets = request_json(
|
||||||
|
app.clone(),
|
||||||
|
"POST",
|
||||||
|
&format!("/api/w/{TEST_WORKSPACE_ID}/tickets/query"),
|
||||||
|
Some(json!({
|
||||||
|
"limit": 1
|
||||||
|
})),
|
||||||
|
StatusCode::OK,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(queried_tickets["items"][0]["title"], "API Ticket");
|
||||||
|
let queried_ticket_id = queried_tickets["items"][0]["id"]
|
||||||
|
.as_str()
|
||||||
|
.expect("query Ticket id")
|
||||||
|
.to_string();
|
||||||
|
assert_eq!(queried_tickets["page"]["limit"], 1);
|
||||||
|
let shown_ticket = request_json(
|
||||||
|
app.clone(),
|
||||||
|
"POST",
|
||||||
|
&format!("/api/w/{TEST_WORKSPACE_ID}/tickets/{queried_ticket_id}/show"),
|
||||||
|
Some(json!({"event_limit": 10})),
|
||||||
|
StatusCode::OK,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(shown_ticket["evidence"]["missing"].is_array());
|
||||||
|
assert!(shown_ticket["item_revision"].as_str().is_some());
|
||||||
|
let queried_objectives = request_json(
|
||||||
|
app.clone(),
|
||||||
|
"POST",
|
||||||
|
&format!("/api/w/{TEST_WORKSPACE_ID}/objectives/query"),
|
||||||
|
Some(json!({
|
||||||
|
"text": "Objective body",
|
||||||
|
"linked_ticket_id": "00000000001J2",
|
||||||
|
"limit": 1
|
||||||
|
})),
|
||||||
|
StatusCode::OK,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(queried_objectives["items"][0]["id"], "00000000001J3");
|
||||||
|
assert_eq!(queried_objectives["page"]["limit"], 1);
|
||||||
|
let shown_objective = request_json(
|
||||||
|
app.clone(),
|
||||||
|
"POST",
|
||||||
|
&format!("/api/w/{TEST_WORKSPACE_ID}/objectives/00000000001J3/show"),
|
||||||
|
Some(json!({"event_limit": 10})),
|
||||||
|
StatusCode::OK,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(shown_objective["linked_tickets"][0], "00000000001J2");
|
||||||
|
assert!(shown_objective["event_page"]["returned"].is_number());
|
||||||
|
|
||||||
let memory_document =
|
let memory_document =
|
||||||
get_json(app.clone(), &format!("/api/w/{TEST_WORKSPACE_ID}/memory")).await;
|
get_json(app.clone(), &format!("/api/w/{TEST_WORKSPACE_ID}/memory")).await;
|
||||||
|
|||||||
@@ -633,6 +633,12 @@ pub trait ControlPlaneStore: Send + Sync {
|
|||||||
|
|
||||||
fn upsert_objective(&self, record: &ObjectiveRecord) -> Result<()>;
|
fn upsert_objective(&self, record: &ObjectiveRecord) -> Result<()>;
|
||||||
fn list_objectives(&self, workspace_id: &str, limit: usize) -> Result<Vec<ObjectiveRecord>>;
|
fn list_objectives(&self, workspace_id: &str, limit: usize) -> Result<Vec<ObjectiveRecord>>;
|
||||||
|
fn list_objectives_for_ticket(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
ticket_id: &str,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<ObjectiveRecord>>;
|
||||||
fn get_objective(
|
fn get_objective(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
@@ -1552,6 +1558,33 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_objectives_for_ticket(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
ticket_id: &str,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<ObjectiveRecord>> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
r#"SELECT o.workspace_id, o.objective_id, o.title, o.state, o.body_md,
|
||||||
|
o.created_at, o.updated_at
|
||||||
|
FROM objectives AS o
|
||||||
|
INNER JOIN objective_ticket_links AS l
|
||||||
|
ON l.workspace_id = o.workspace_id
|
||||||
|
AND l.objective_id = o.objective_id
|
||||||
|
WHERE o.workspace_id = ?1 AND l.ticket_id = ?2
|
||||||
|
ORDER BY o.updated_at DESC, o.objective_id ASC
|
||||||
|
LIMIT ?3"#,
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(
|
||||||
|
params![workspace_id, ticket_id, limit as i64],
|
||||||
|
read_objective_record,
|
||||||
|
)?;
|
||||||
|
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.map_err(Error::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn get_objective(
|
fn get_objective(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
|
|||||||
@@ -23,8 +23,19 @@ export type TicketListResponse = {
|
|||||||
record_authority: string;
|
record_authority: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type QueryPage = {
|
||||||
|
limit: number;
|
||||||
|
returned: number;
|
||||||
|
has_more: boolean;
|
||||||
|
next_cursor: string | null;
|
||||||
|
sort: string;
|
||||||
|
source_limit: number | null;
|
||||||
|
source_truncated: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type TicketEventDetail = {
|
export type TicketEventDetail = {
|
||||||
sequence: number;
|
sequence: number;
|
||||||
|
event_ref: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
author: string | null;
|
author: string | null;
|
||||||
at: string | null;
|
at: string | null;
|
||||||
@@ -35,6 +46,89 @@ export type TicketEventDetail = {
|
|||||||
state_field: string | null;
|
state_field: string | null;
|
||||||
heading: string | null;
|
heading: string | null;
|
||||||
body: string | null;
|
body: string | null;
|
||||||
|
attributes: { [key in string]: string };
|
||||||
|
references: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ObjectiveLinkSummary = { id: string; title: string; state: string };
|
||||||
|
|
||||||
|
export type TicketEvidenceEvent = {
|
||||||
|
event_ref: string;
|
||||||
|
sequence: number;
|
||||||
|
kind: string;
|
||||||
|
at: string | null;
|
||||||
|
author: string | null;
|
||||||
|
excerpt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketMergeRequestSummary = {
|
||||||
|
merge_request_id: string;
|
||||||
|
state: string;
|
||||||
|
review_status: string;
|
||||||
|
revision_id: string;
|
||||||
|
base_commit: string;
|
||||||
|
head_commit: string;
|
||||||
|
changed_paths: Array<string>;
|
||||||
|
updated_at: string;
|
||||||
|
review_submitted_at: string | null;
|
||||||
|
review_excerpt: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketEvidenceSummary = {
|
||||||
|
has_implementation_report: boolean;
|
||||||
|
implementation_report_after_rescope: boolean;
|
||||||
|
has_merge_request: boolean;
|
||||||
|
has_commit: boolean;
|
||||||
|
review_status: string | null;
|
||||||
|
approved: boolean;
|
||||||
|
unresolved_request_changes: boolean;
|
||||||
|
complete_for_integration: boolean;
|
||||||
|
missing: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketQueryRequest = {
|
||||||
|
text: string | null;
|
||||||
|
states: Array<string>;
|
||||||
|
event_kinds: Array<string>;
|
||||||
|
evidence: Array<string>;
|
||||||
|
review_status: string | null;
|
||||||
|
attention: Array<string>;
|
||||||
|
related_ticket_id: string | null;
|
||||||
|
relation_kind: string | null;
|
||||||
|
linked_objective_id: string | null;
|
||||||
|
updated_after: string | null;
|
||||||
|
updated_before: string | null;
|
||||||
|
sort: string | null;
|
||||||
|
limit: number | null;
|
||||||
|
cursor: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketQueryItem = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
priority: string;
|
||||||
|
updated_at: string | null;
|
||||||
|
workspace_action_priority: string;
|
||||||
|
matched_fields: Array<string>;
|
||||||
|
snippet: string | null;
|
||||||
|
matching_event: TicketEvidenceEvent | null;
|
||||||
|
linked_objective_ids: Array<string>;
|
||||||
|
relation_count: number;
|
||||||
|
blocker_count: number;
|
||||||
|
evidence: TicketEvidenceSummary;
|
||||||
|
merge_request: TicketMergeRequestSummary | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketQueryResponse = {
|
||||||
|
items: Array<TicketQueryItem>;
|
||||||
|
page: QueryPage;
|
||||||
|
record_authority: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketShowRequest = {
|
||||||
|
event_limit: number | null;
|
||||||
|
event_cursor: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TicketRelation = {
|
export type TicketRelation = {
|
||||||
@@ -83,6 +177,7 @@ export type TicketDetail = {
|
|||||||
priority: string;
|
priority: string;
|
||||||
created_at: string | null;
|
created_at: string | null;
|
||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
|
item_revision: string;
|
||||||
queued_by: string | null;
|
queued_by: string | null;
|
||||||
queued_at: string | null;
|
queued_at: string | null;
|
||||||
assignee: string | null;
|
assignee: string | null;
|
||||||
@@ -93,9 +188,14 @@ export type TicketDetail = {
|
|||||||
body_truncated: boolean;
|
body_truncated: boolean;
|
||||||
event_count: number;
|
event_count: number;
|
||||||
events: Array<TicketEventDetail>;
|
events: Array<TicketEventDetail>;
|
||||||
|
event_page: QueryPage;
|
||||||
artifact_count: number;
|
artifact_count: number;
|
||||||
artifacts: Array<string>;
|
artifacts: Array<string>;
|
||||||
relations: TicketRelationView;
|
relations: TicketRelationView;
|
||||||
|
linked_objectives: Array<ObjectiveLinkSummary>;
|
||||||
|
implementation_reports: Array<TicketEvidenceEvent>;
|
||||||
|
merge_request: TicketMergeRequestSummary | null;
|
||||||
|
evidence: TicketEvidenceSummary;
|
||||||
resolution: string | null;
|
resolution: string | null;
|
||||||
record_source: string;
|
record_source: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user