fix: resolve human keys at resource boundaries

This commit is contained in:
2026-08-20 02:30:09 +09:00
parent a96038d79f
commit 97960d4e3f
+96 -39
View File
@@ -698,11 +698,20 @@ impl SqliteWorkspaceAuthority {
}) })
} }
fn read_ticket_detail(&self, id: &str, request: TicketShowRequest) -> Result<TicketDetail> { fn read_ticket_detail(
validate_project_id(id)?; &self,
let ticket = self reference: &str,
.ticket_backend request: TicketShowRequest,
.show(TicketIdOrSlug::Id(id.to_string()))?; ) -> Result<TicketDetail> {
let id = self
.store
.resolve_resource_reference(
&self.workspace_id,
WorkspaceResourceKind::Ticket,
reference,
)?
.ok_or_else(|| Error::Ticket(ticket::TicketError::NotFound(reference.to_string())))?;
let ticket = self.ticket_backend.show(TicketIdOrSlug::Id(id))?;
self.ticket_detail_from_ticket(ticket, request) self.ticket_detail_from_ticket(ticket, request)
} }
@@ -1147,15 +1156,21 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
}) })
} }
fn objective(&self, id: &str) -> Result<ObjectiveDetail> { fn objective(&self, reference: &str) -> Result<ObjectiveDetail> {
validate_project_id(id)?; let record = self.objective_record(reference)?;
let record = self.objective_record(id)?;
self.objective_detail_from_record(record) self.objective_detail_from_record(record)
} }
fn show_objective(&self, id: &str, query: ObjectiveShowRequest) -> Result<ObjectiveDetail> { fn show_objective(
let mut detail = self.objective(id)?; &self,
let all_events = self.store.list_objective_events(&self.workspace_id, id)?; reference: &str,
query: ObjectiveShowRequest,
) -> Result<ObjectiveDetail> {
let mut detail = self.objective(reference)?;
let objective_id = detail.id.clone();
let all_events = self
.store
.list_objective_events(&self.workspace_id, &objective_id)?;
let event_limit = query let event_limit = query
.event_limit .event_limit
.unwrap_or(TICKET_EVENT_LIMIT) .unwrap_or(TICKET_EVENT_LIMIT)
@@ -1240,9 +1255,12 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
self.objective(&objective_id) self.objective(&objective_id)
} }
fn edit_objective(&self, id: &str, input: ObjectiveEditInput) -> Result<ObjectiveDetail> { fn edit_objective(
validate_project_id(id)?; &self,
let mut record = self.objective_record(id)?; reference: &str,
input: ObjectiveEditInput,
) -> Result<ObjectiveDetail> {
let mut record = self.objective_record(reference)?;
let mut changed = false; let mut changed = false;
if let Some(title) = input.title { if let Some(title) = input.title {
validate_objective_title(&title)?; validate_objective_title(&title)?;
@@ -1288,60 +1306,85 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
)); ));
} }
record.updated_at = now_rfc3339(); record.updated_at = now_rfc3339();
let objective_id = record.objective_id.clone();
self.store.upsert_objective(&record)?; self.store.upsert_objective(&record)?;
self.insert_objective_event(id, "edit", None)?; self.insert_objective_event(&objective_id, "edit", None)?;
self.objective(id) self.objective(&objective_id)
} }
fn set_objective_state(&self, id: &str, state: &str) -> Result<ObjectiveDetail> { fn set_objective_state(&self, reference: &str, state: &str) -> Result<ObjectiveDetail> {
validate_project_id(id)?;
validate_objective_state(state)?; validate_objective_state(state)?;
let mut record = self.objective_record(id)?; let mut record = self.objective_record(reference)?;
record.state = state.trim().to_string(); record.state = state.trim().to_string();
record.updated_at = now_rfc3339(); record.updated_at = now_rfc3339();
let objective_id = record.objective_id.clone();
self.store.upsert_objective(&record)?; self.store.upsert_objective(&record)?;
self.insert_objective_event(id, "state", Some(&record.state))?; self.insert_objective_event(&objective_id, "state", Some(&record.state))?;
self.objective(id) self.objective(&objective_id)
} }
fn link_objective_ticket(&self, id: &str, ticket_id: &str) -> Result<ObjectiveDetail> { fn link_objective_ticket(
validate_project_id(id)?; &self,
validate_project_id(ticket_id)?; objective_reference: &str,
let _record = self.objective_record(id)?; ticket_reference: &str,
) -> Result<ObjectiveDetail> {
let objective_id = self.objective_record(objective_reference)?.objective_id;
let ticket_id = self
.store
.resolve_resource_reference(
&self.workspace_id,
WorkspaceResourceKind::Ticket,
ticket_reference,
)?
.ok_or_else(|| {
Error::Ticket(ticket::TicketError::NotFound(ticket_reference.to_string()))
})?;
let now = now_rfc3339(); let now = now_rfc3339();
let mut links = self let mut links = self
.store .store
.list_objective_ticket_links(&self.workspace_id, id)?; .list_objective_ticket_links(&self.workspace_id, &objective_id)?;
if !links.iter().any(|link| link.ticket_id == ticket_id) { if !links.iter().any(|link| link.ticket_id == ticket_id) {
links.push(ObjectiveTicketLinkRecord { links.push(ObjectiveTicketLinkRecord {
workspace_id: self.workspace_id.clone(), workspace_id: self.workspace_id.clone(),
objective_id: id.to_string(), objective_id: objective_id.clone(),
ticket_id: ticket_id.to_string(), ticket_id: ticket_id.clone(),
kind: "linked".to_string(), kind: "linked".to_string(),
created_at: now, created_at: now,
}); });
self.store self.store
.replace_objective_ticket_links(&self.workspace_id, id, &links)?; .replace_objective_ticket_links(&self.workspace_id, &objective_id, &links)?;
self.insert_objective_event(id, "link_ticket", Some(ticket_id))?; self.insert_objective_event(&objective_id, "link_ticket", Some(&ticket_id))?;
} }
self.objective(id) self.objective(&objective_id)
} }
fn unlink_objective_ticket(&self, id: &str, ticket_id: &str) -> Result<ObjectiveDetail> { fn unlink_objective_ticket(
validate_project_id(id)?; &self,
validate_project_id(ticket_id)?; objective_reference: &str,
let _record = self.objective_record(id)?; ticket_reference: &str,
) -> Result<ObjectiveDetail> {
let objective_id = self.objective_record(objective_reference)?.objective_id;
let ticket_id = self
.store
.resolve_resource_reference(
&self.workspace_id,
WorkspaceResourceKind::Ticket,
ticket_reference,
)?
.ok_or_else(|| {
Error::Ticket(ticket::TicketError::NotFound(ticket_reference.to_string()))
})?;
let mut links = self let mut links = self
.store .store
.list_objective_ticket_links(&self.workspace_id, id)?; .list_objective_ticket_links(&self.workspace_id, &objective_id)?;
let original_len = links.len(); let original_len = links.len();
links.retain(|link| link.ticket_id != ticket_id); links.retain(|link| link.ticket_id != ticket_id);
if links.len() != original_len { if links.len() != original_len {
self.store self.store
.replace_objective_ticket_links(&self.workspace_id, id, &links)?; .replace_objective_ticket_links(&self.workspace_id, &objective_id, &links)?;
self.insert_objective_event(id, "unlink_ticket", Some(ticket_id))?; self.insert_objective_event(&objective_id, "unlink_ticket", Some(&ticket_id))?;
} }
self.objective(id) self.objective(&objective_id)
} }
} }
@@ -2907,6 +2950,8 @@ mod tests {
assert_eq!(tickets.items[0].id, "00000000001J2"); assert_eq!(tickets.items[0].id, "00000000001J2");
assert_eq!(tickets.items[0].state, "ready"); assert_eq!(tickets.items[0].state, "ready");
assert_eq!(tickets.items[0].workspace_action_priority, "background"); assert_eq!(tickets.items[0].workspace_action_priority, "background");
let ticket_by_key = authority.ticket(&tickets.items[0].human_key).unwrap();
assert_eq!(ticket_by_key.id, tickets.items[0].id);
let ticket = authority.ticket("00000000001J2").unwrap(); let ticket = authority.ticket("00000000001J2").unwrap();
assert!(ticket.body.contains("Ticket body")); assert!(ticket.body.contains("Ticket body"));
@@ -3078,6 +3123,18 @@ mod tests {
assert_eq!(objectives.items.len(), 1); assert_eq!(objectives.items.len(), 1);
assert_eq!(objectives.items[0].id, "00000000001J3"); assert_eq!(objectives.items[0].id, "00000000001J3");
assert_eq!(objectives.items[0].linked_tickets, vec!["00000000001J2"]); assert_eq!(objectives.items[0].linked_tickets, vec!["00000000001J2"]);
let objective_by_key = authority.objective(&objectives.items[0].human_key).unwrap();
assert_eq!(objective_by_key.id, objectives.items[0].id);
assert_eq!(
authority
.show_objective(
&objectives.items[0].human_key,
ObjectiveShowRequest::default(),
)
.unwrap()
.id,
objectives.items[0].id
);
let objective = authority.objective("00000000001J3").unwrap(); let objective = authority.objective("00000000001J3").unwrap();
assert!(objective.body.contains("Objective body")); assert!(objective.body.contains("Objective body"));