1use serde::{Deserialize, Serialize};
10
11#[cfg(feature = "server")]
12pub mod crypto;
13#[cfg(feature = "server")]
14pub mod db;
15pub mod oauth;
16#[cfg(feature = "server")]
17pub mod service;
18
19pub use opensession_core::trace::{
21 Agent, Content, ContentBlock, Event, EventType, Session, SessionContext, Stats,
22};
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "snake_case")]
29#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
30#[cfg_attr(feature = "ts", ts(export))]
31pub enum TeamRole {
32 Admin,
33 Member,
34}
35
36impl TeamRole {
37 pub fn as_str(&self) -> &str {
38 match self {
39 Self::Admin => "admin",
40 Self::Member => "member",
41 }
42 }
43}
44
45impl std::fmt::Display for TeamRole {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.write_str(self.as_str())
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "snake_case")]
54#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
55#[cfg_attr(feature = "ts", ts(export))]
56pub enum InvitationStatus {
57 Pending,
58 Accepted,
59 Declined,
60}
61
62impl InvitationStatus {
63 pub fn as_str(&self) -> &str {
64 match self {
65 Self::Pending => "pending",
66 Self::Accepted => "accepted",
67 Self::Declined => "declined",
68 }
69 }
70}
71
72impl std::fmt::Display for InvitationStatus {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.write_str(self.as_str())
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
80#[serde(rename_all = "snake_case")]
81#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
82#[cfg_attr(feature = "ts", ts(export))]
83pub enum SortOrder {
84 #[default]
85 Recent,
86 Popular,
87 Longest,
88}
89
90impl SortOrder {
91 pub fn as_str(&self) -> &str {
92 match self {
93 Self::Recent => "recent",
94 Self::Popular => "popular",
95 Self::Longest => "longest",
96 }
97 }
98}
99
100impl std::fmt::Display for SortOrder {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.write_str(self.as_str())
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
108#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
109#[cfg_attr(feature = "ts", ts(export))]
110pub enum TimeRange {
111 #[serde(rename = "24h")]
112 Hours24,
113 #[serde(rename = "7d")]
114 Days7,
115 #[serde(rename = "30d")]
116 Days30,
117 #[default]
118 #[serde(rename = "all")]
119 All,
120}
121
122impl TimeRange {
123 pub fn as_str(&self) -> &str {
124 match self {
125 Self::Hours24 => "24h",
126 Self::Days7 => "7d",
127 Self::Days30 => "30d",
128 Self::All => "all",
129 }
130 }
131}
132
133impl std::fmt::Display for TimeRange {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 f.write_str(self.as_str())
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141#[serde(rename_all = "snake_case")]
142#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
143#[cfg_attr(feature = "ts", ts(export))]
144pub enum LinkType {
145 Handoff,
146 Related,
147 Parent,
148 Child,
149}
150
151impl LinkType {
152 pub fn as_str(&self) -> &str {
153 match self {
154 Self::Handoff => "handoff",
155 Self::Related => "related",
156 Self::Parent => "parent",
157 Self::Child => "child",
158 }
159 }
160}
161
162impl std::fmt::Display for LinkType {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.write_str(self.as_str())
165 }
166}
167
168pub fn saturating_i64(v: u64) -> i64 {
172 i64::try_from(v).unwrap_or(i64::MAX)
173}
174
175#[derive(Debug, Deserialize)]
179#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
180#[cfg_attr(feature = "ts", ts(export))]
181pub struct RegisterRequest {
182 pub nickname: String,
183}
184
185#[derive(Debug, Serialize, Deserialize)]
187#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
188#[cfg_attr(feature = "ts", ts(export))]
189pub struct AuthRegisterRequest {
190 pub email: String,
191 pub password: String,
192 pub nickname: String,
193}
194
195#[derive(Debug, Serialize, Deserialize)]
197#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
198#[cfg_attr(feature = "ts", ts(export))]
199pub struct LoginRequest {
200 pub email: String,
201 pub password: String,
202}
203
204#[derive(Debug, Serialize, Deserialize)]
206#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
207#[cfg_attr(feature = "ts", ts(export))]
208pub struct AuthTokenResponse {
209 pub access_token: String,
210 pub refresh_token: String,
211 pub expires_in: u64,
212 pub user_id: String,
213 pub nickname: String,
214}
215
216#[derive(Debug, Serialize, Deserialize)]
218#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
219#[cfg_attr(feature = "ts", ts(export))]
220pub struct RefreshRequest {
221 pub refresh_token: String,
222}
223
224#[derive(Debug, Serialize, Deserialize)]
226#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
227#[cfg_attr(feature = "ts", ts(export))]
228pub struct LogoutRequest {
229 pub refresh_token: String,
230}
231
232#[derive(Debug, Serialize, Deserialize)]
234#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
235#[cfg_attr(feature = "ts", ts(export))]
236pub struct ChangePasswordRequest {
237 pub current_password: String,
238 pub new_password: String,
239}
240
241#[derive(Debug, Serialize)]
243#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
244#[cfg_attr(feature = "ts", ts(export))]
245pub struct RegisterResponse {
246 pub user_id: String,
247 pub nickname: String,
248 pub api_key: String,
249}
250
251#[derive(Debug, Serialize, Deserialize)]
253#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
254#[cfg_attr(feature = "ts", ts(export))]
255pub struct VerifyResponse {
256 pub user_id: String,
257 pub nickname: String,
258}
259
260#[derive(Debug, Serialize, Deserialize)]
262#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
263#[cfg_attr(feature = "ts", ts(export))]
264pub struct UserSettingsResponse {
265 pub user_id: String,
266 pub nickname: String,
267 pub api_key: String,
268 pub created_at: String,
269 pub email: Option<String>,
270 pub avatar_url: Option<String>,
271 #[serde(default)]
273 pub oauth_providers: Vec<oauth::LinkedProvider>,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub github_username: Option<String>,
277}
278
279#[derive(Debug, Serialize, Deserialize)]
281#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
282#[cfg_attr(feature = "ts", ts(export))]
283pub struct OkResponse {
284 pub ok: bool,
285}
286
287#[derive(Debug, Serialize, Deserialize)]
289#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
290#[cfg_attr(feature = "ts", ts(export))]
291pub struct RegenerateKeyResponse {
292 pub api_key: String,
293}
294
295#[derive(Debug, Serialize)]
297#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
298#[cfg_attr(feature = "ts", ts(export))]
299pub struct OAuthLinkResponse {
300 pub url: String,
301}
302
303#[derive(Debug, Serialize, Deserialize)]
307pub struct UploadRequest {
308 pub session: Session,
309 pub team_id: Option<String>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub body_url: Option<String>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub linked_session_ids: Option<Vec<String>>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub git_remote: Option<String>,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub git_branch: Option<String>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub git_commit: Option<String>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub git_repo_name: Option<String>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub pr_number: Option<i64>,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub pr_url: Option<String>,
326}
327
328#[derive(Debug, Serialize, Deserialize)]
330#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
331#[cfg_attr(feature = "ts", ts(export))]
332pub struct UploadResponse {
333 pub id: String,
334 pub url: String,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
340#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
341#[cfg_attr(feature = "ts", ts(export))]
342pub struct SessionSummary {
343 pub id: String,
344 pub user_id: Option<String>,
345 pub nickname: Option<String>,
346 pub team_id: String,
347 pub tool: String,
348 pub agent_provider: Option<String>,
349 pub agent_model: Option<String>,
350 pub title: Option<String>,
351 pub description: Option<String>,
352 pub tags: Option<String>,
354 pub created_at: String,
355 pub uploaded_at: String,
356 pub message_count: i64,
357 pub task_count: i64,
358 pub event_count: i64,
359 pub duration_seconds: i64,
360 pub total_input_tokens: i64,
361 pub total_output_tokens: i64,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub git_remote: Option<String>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub git_branch: Option<String>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub git_commit: Option<String>,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub git_repo_name: Option<String>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub pr_number: Option<i64>,
372 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub pr_url: Option<String>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub working_directory: Option<String>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub files_modified: Option<String>,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub files_read: Option<String>,
380 #[serde(default)]
381 pub has_errors: bool,
382}
383
384#[derive(Debug, Serialize, Deserialize)]
386#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
387#[cfg_attr(feature = "ts", ts(export))]
388pub struct SessionListResponse {
389 pub sessions: Vec<SessionSummary>,
390 pub total: i64,
391 pub page: u32,
392 pub per_page: u32,
393}
394
395#[derive(Debug, Deserialize)]
397#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
398#[cfg_attr(feature = "ts", ts(export))]
399pub struct SessionListQuery {
400 #[serde(default = "default_page")]
401 pub page: u32,
402 #[serde(default = "default_per_page")]
403 pub per_page: u32,
404 pub search: Option<String>,
405 pub tool: Option<String>,
406 pub team_id: Option<String>,
407 pub sort: Option<SortOrder>,
409 pub time_range: Option<TimeRange>,
411}
412
413fn default_page() -> u32 {
414 1
415}
416fn default_per_page() -> u32 {
417 20
418}
419
420#[derive(Debug, Serialize, Deserialize)]
422#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
423#[cfg_attr(feature = "ts", ts(export))]
424pub struct SessionDetail {
425 #[serde(flatten)]
426 #[cfg_attr(feature = "ts", ts(flatten))]
427 pub summary: SessionSummary,
428 pub team_name: Option<String>,
429 #[serde(default, skip_serializing_if = "Vec::is_empty")]
430 pub linked_sessions: Vec<SessionLink>,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
436#[cfg_attr(feature = "ts", ts(export))]
437pub struct SessionLink {
438 pub session_id: String,
439 pub linked_session_id: String,
440 pub link_type: LinkType,
441 pub created_at: String,
442}
443
444#[derive(Debug, Serialize, Deserialize)]
448#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
449#[cfg_attr(feature = "ts", ts(export))]
450pub struct CreateTeamRequest {
451 pub name: String,
452 pub description: Option<String>,
453 pub is_public: Option<bool>,
454}
455
456#[derive(Debug, Serialize, Deserialize)]
458#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
459#[cfg_attr(feature = "ts", ts(export))]
460pub struct TeamResponse {
461 pub id: String,
462 pub name: String,
463 pub description: Option<String>,
464 pub is_public: bool,
465 pub created_by: String,
466 pub created_at: String,
467}
468
469#[derive(Debug, Serialize, Deserialize)]
471#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
472#[cfg_attr(feature = "ts", ts(export))]
473pub struct ListTeamsResponse {
474 pub teams: Vec<TeamResponse>,
475}
476
477#[derive(Debug, Serialize, Deserialize)]
479#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
480#[cfg_attr(feature = "ts", ts(export))]
481pub struct TeamDetailResponse {
482 #[serde(flatten)]
483 #[cfg_attr(feature = "ts", ts(flatten))]
484 pub team: TeamResponse,
485 pub member_count: i64,
486 pub sessions: Vec<SessionSummary>,
487}
488
489#[derive(Debug, Serialize, Deserialize)]
491#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
492#[cfg_attr(feature = "ts", ts(export))]
493pub struct UpdateTeamRequest {
494 pub name: Option<String>,
495 pub description: Option<String>,
496 pub is_public: Option<bool>,
497}
498
499#[derive(Debug, Deserialize)]
501#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
502#[cfg_attr(feature = "ts", ts(export))]
503pub struct TeamStatsQuery {
504 pub time_range: Option<TimeRange>,
506}
507
508#[derive(Debug, Serialize)]
510#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
511#[cfg_attr(feature = "ts", ts(export))]
512pub struct TeamStatsResponse {
513 pub team_id: String,
514 pub time_range: TimeRange,
515 pub totals: TeamStatsTotals,
516 pub by_user: Vec<UserStats>,
517 pub by_tool: Vec<ToolStats>,
518}
519
520#[derive(Debug, Serialize)]
522#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
523#[cfg_attr(feature = "ts", ts(export))]
524pub struct TeamStatsTotals {
525 pub session_count: i64,
526 pub message_count: i64,
527 pub event_count: i64,
528 pub tool_call_count: i64,
529 pub duration_seconds: i64,
530 pub total_input_tokens: i64,
531 pub total_output_tokens: i64,
532}
533
534#[derive(Debug, Serialize)]
536#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
537#[cfg_attr(feature = "ts", ts(export))]
538pub struct UserStats {
539 pub user_id: String,
540 pub nickname: String,
541 pub session_count: i64,
542 pub message_count: i64,
543 pub event_count: i64,
544 pub duration_seconds: i64,
545 pub total_input_tokens: i64,
546 pub total_output_tokens: i64,
547}
548
549#[derive(Debug, Serialize)]
551#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
552#[cfg_attr(feature = "ts", ts(export))]
553pub struct ToolStats {
554 pub tool: String,
555 pub session_count: i64,
556 pub message_count: i64,
557 pub event_count: i64,
558 pub duration_seconds: i64,
559 pub total_input_tokens: i64,
560 pub total_output_tokens: i64,
561}
562
563impl From<opensession_core::stats::SessionAggregate> for TeamStatsTotals {
566 fn from(a: opensession_core::stats::SessionAggregate) -> Self {
567 Self {
568 session_count: saturating_i64(a.session_count),
569 message_count: saturating_i64(a.message_count),
570 event_count: saturating_i64(a.event_count),
571 tool_call_count: saturating_i64(a.tool_call_count),
572 duration_seconds: saturating_i64(a.duration_seconds),
573 total_input_tokens: saturating_i64(a.total_input_tokens),
574 total_output_tokens: saturating_i64(a.total_output_tokens),
575 }
576 }
577}
578
579impl From<(String, opensession_core::stats::SessionAggregate)> for ToolStats {
580 fn from((tool, a): (String, opensession_core::stats::SessionAggregate)) -> Self {
581 Self {
582 tool,
583 session_count: saturating_i64(a.session_count),
584 message_count: saturating_i64(a.message_count),
585 event_count: saturating_i64(a.event_count),
586 duration_seconds: saturating_i64(a.duration_seconds),
587 total_input_tokens: saturating_i64(a.total_input_tokens),
588 total_output_tokens: saturating_i64(a.total_output_tokens),
589 }
590 }
591}
592
593#[derive(Debug, Serialize, Deserialize)]
597#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
598#[cfg_attr(feature = "ts", ts(export))]
599pub struct InviteRequest {
600 pub email: Option<String>,
601 pub oauth_provider: Option<String>,
603 pub oauth_provider_username: Option<String>,
605 pub role: Option<TeamRole>,
606}
607
608#[derive(Debug, Serialize, Deserialize)]
610#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
611#[cfg_attr(feature = "ts", ts(export))]
612pub struct InvitationResponse {
613 pub id: String,
614 pub team_id: String,
615 pub team_name: String,
616 pub email: Option<String>,
617 pub oauth_provider: Option<String>,
618 pub oauth_provider_username: Option<String>,
619 pub invited_by_nickname: String,
620 pub role: TeamRole,
621 pub status: InvitationStatus,
622 pub created_at: String,
623}
624
625#[derive(Debug, Serialize, Deserialize)]
627#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
628#[cfg_attr(feature = "ts", ts(export))]
629pub struct ListInvitationsResponse {
630 pub invitations: Vec<InvitationResponse>,
631}
632
633#[derive(Debug, Serialize, Deserialize)]
635#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
636#[cfg_attr(feature = "ts", ts(export))]
637pub struct AcceptInvitationResponse {
638 pub team_id: String,
639 pub role: TeamRole,
640}
641
642#[derive(Debug, Serialize, Deserialize)]
646#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
647#[cfg_attr(feature = "ts", ts(export))]
648pub struct AddMemberRequest {
649 pub nickname: String,
650}
651
652#[derive(Debug, Serialize, Deserialize)]
654#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
655#[cfg_attr(feature = "ts", ts(export))]
656pub struct MemberResponse {
657 pub user_id: String,
658 pub nickname: String,
659 pub role: TeamRole,
660 pub joined_at: String,
661}
662
663#[derive(Debug, Serialize, Deserialize)]
665#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
666#[cfg_attr(feature = "ts", ts(export))]
667pub struct ListMembersResponse {
668 pub members: Vec<MemberResponse>,
669}
670
671#[derive(Debug, Serialize, Deserialize)]
675#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
676#[cfg_attr(feature = "ts", ts(export))]
677pub struct ConfigSyncResponse {
678 pub privacy: Option<SyncedPrivacyConfig>,
679 pub watchers: Option<SyncedWatcherConfig>,
680}
681
682#[derive(Debug, Serialize, Deserialize)]
684#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
685#[cfg_attr(feature = "ts", ts(export))]
686pub struct SyncedPrivacyConfig {
687 pub exclude_patterns: Option<Vec<String>>,
688 pub exclude_tools: Option<Vec<String>>,
689}
690
691#[derive(Debug, Serialize, Deserialize)]
693#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
694#[cfg_attr(feature = "ts", ts(export))]
695pub struct SyncedWatcherConfig {
696 pub claude_code: Option<bool>,
697 pub opencode: Option<bool>,
698 pub goose: Option<bool>,
699 pub aider: Option<bool>,
700 pub cursor: Option<bool>,
701}
702
703#[derive(Debug, Deserialize)]
707pub struct SyncPullQuery {
708 pub team_id: String,
709 pub since: Option<String>,
711 pub limit: Option<u32>,
713}
714
715#[derive(Debug, Serialize, Deserialize)]
717pub struct SyncPullResponse {
718 pub sessions: Vec<SessionSummary>,
719 pub next_cursor: Option<String>,
721 pub has_more: bool,
722}
723
724#[derive(Debug, Deserialize)]
728#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
729#[cfg_attr(feature = "ts", ts(export))]
730pub struct StreamEventsRequest {
731 #[cfg_attr(feature = "ts", ts(type = "any"))]
732 pub agent: Option<Agent>,
733 #[cfg_attr(feature = "ts", ts(type = "any"))]
734 pub context: Option<SessionContext>,
735 #[cfg_attr(feature = "ts", ts(type = "any[]"))]
736 pub events: Vec<Event>,
737}
738
739#[derive(Debug, Serialize, Deserialize)]
741#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
742#[cfg_attr(feature = "ts", ts(export))]
743pub struct StreamEventsResponse {
744 pub accepted: usize,
745}
746
747#[derive(Debug, Serialize, Deserialize)]
751#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
752#[cfg_attr(feature = "ts", ts(export))]
753pub struct HealthResponse {
754 pub status: String,
755 pub version: String,
756}
757
758#[derive(Debug, Clone)]
765#[non_exhaustive]
766pub enum ServiceError {
767 BadRequest(String),
768 Unauthorized(String),
769 Forbidden(String),
770 NotFound(String),
771 Conflict(String),
772 Internal(String),
773}
774
775impl ServiceError {
776 pub fn status_code(&self) -> u16 {
778 match self {
779 Self::BadRequest(_) => 400,
780 Self::Unauthorized(_) => 401,
781 Self::Forbidden(_) => 403,
782 Self::NotFound(_) => 404,
783 Self::Conflict(_) => 409,
784 Self::Internal(_) => 500,
785 }
786 }
787
788 pub fn message(&self) -> &str {
790 match self {
791 Self::BadRequest(m)
792 | Self::Unauthorized(m)
793 | Self::Forbidden(m)
794 | Self::NotFound(m)
795 | Self::Conflict(m)
796 | Self::Internal(m) => m,
797 }
798 }
799
800 pub fn from_db<E: std::fmt::Display>(context: &str) -> impl FnOnce(E) -> Self + '_ {
802 move |e| Self::Internal(format!("{context}: {e}"))
803 }
804}
805
806impl std::fmt::Display for ServiceError {
807 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808 write!(f, "{}", self.message())
809 }
810}
811
812impl std::error::Error for ServiceError {}
813
814#[derive(Debug, Serialize)]
818#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
819#[cfg_attr(feature = "ts", ts(export))]
820pub struct ApiError {
821 pub error: String,
822}
823
824impl From<&ServiceError> for ApiError {
825 fn from(e: &ServiceError) -> Self {
826 Self {
827 error: e.message().to_string(),
828 }
829 }
830}
831
832#[cfg(all(test, feature = "ts"))]
835mod tests {
836 use super::*;
837 use std::io::Write;
838 use std::path::PathBuf;
839 use ts_rs::TS;
840
841 #[test]
843 fn export_typescript() {
844 let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
845 .join("../../packages/ui/src/api-types.generated.ts");
846
847 let cfg = ts_rs::Config::new().with_large_int("number");
848 let mut parts: Vec<String> = Vec::new();
849 parts.push("// AUTO-GENERATED by opensession-api-types — DO NOT EDIT".to_string());
850 parts.push(
851 "// Regenerate with: cargo test -p opensession-api-types -- export_typescript"
852 .to_string(),
853 );
854 parts.push(String::new());
855
856 macro_rules! collect_ts {
860 ($($t:ty),+ $(,)?) => {
861 $(
862 let decl = <$t>::decl(&cfg);
863 let decl = if decl.contains(" = {") {
864 decl
866 .replacen("type ", "export interface ", 1)
867 .replace(" = {", " {")
868 .trim_end_matches(';')
869 .to_string()
870 } else {
871 decl
873 .replacen("type ", "export type ", 1)
874 .trim_end_matches(';')
875 .to_string()
876 };
877 parts.push(decl);
878 parts.push(String::new());
879 )+
880 };
881 }
882
883 collect_ts!(
884 TeamRole,
886 InvitationStatus,
887 SortOrder,
888 TimeRange,
889 LinkType,
890 RegisterRequest,
892 AuthRegisterRequest,
893 LoginRequest,
894 AuthTokenResponse,
895 RefreshRequest,
896 LogoutRequest,
897 ChangePasswordRequest,
898 RegisterResponse,
899 VerifyResponse,
900 UserSettingsResponse,
901 OkResponse,
902 RegenerateKeyResponse,
903 OAuthLinkResponse,
904 UploadResponse,
906 SessionSummary,
907 SessionListResponse,
908 SessionListQuery,
909 SessionDetail,
910 SessionLink,
911 CreateTeamRequest,
913 TeamResponse,
914 ListTeamsResponse,
915 TeamDetailResponse,
916 UpdateTeamRequest,
917 TeamStatsQuery,
918 TeamStatsResponse,
919 TeamStatsTotals,
920 UserStats,
921 ToolStats,
922 AddMemberRequest,
924 MemberResponse,
925 ListMembersResponse,
926 InviteRequest,
928 InvitationResponse,
929 ListInvitationsResponse,
930 AcceptInvitationResponse,
931 oauth::AuthProvidersResponse,
933 oauth::OAuthProviderInfo,
934 oauth::LinkedProvider,
935 HealthResponse,
937 ApiError,
938 );
939
940 let content = parts.join("\n");
941
942 if let Some(parent) = out_dir.parent() {
944 std::fs::create_dir_all(parent).ok();
945 }
946 let mut file = std::fs::File::create(&out_dir)
947 .unwrap_or_else(|e| panic!("Failed to create {}: {}", out_dir.display(), e));
948 file.write_all(content.as_bytes())
949 .unwrap_or_else(|e| panic!("Failed to write {}: {}", out_dir.display(), e));
950
951 println!("Generated TypeScript types at: {}", out_dir.display());
952 }
953}