1use axum::{
6 extract::{Path, State},
7 response::Json as JsonResponse,
8};
9use tokio::task;
10
11use crate::http::{error::HttpError, state::AppState};
12use crate::storage::{
13 StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
14};
15
16use serde::Serialize;
19
20#[derive(Debug, Serialize, utoipa::ToSchema)]
21pub struct ChangeInfo {
22 pub change_type: String,
23 pub description: String,
24 pub details: std::collections::HashMap<String, String>,
25}
26
27pub(crate) fn parse_is_edge_param(
29 query: &std::collections::HashMap<String, String>,
30) -> Result<bool, HttpError> {
31 match query.get("is_edge") {
32 None => Ok(false),
33 Some(v) => match v.to_lowercase().as_str() {
34 "true" => Ok(true),
35 "false" => Ok(false),
36 _ => Err(HttpError::BadRequest(format!(
37 "Invalid is_edge value: '{}'. Expected 'true' or 'false'",
38 v
39 ))),
40 },
41 }
42}
43
44#[utoipa::path(
45 get,
46 path = "/v1/schema/versions/{space}/{label}",
47 tag = "Schema",
48 params(
49 ("space" = String, Path, description = "Space name"),
50 ("label" = String, Path, description = "Tag or edge type name"),
51 ("is_edge" = Option<bool>, Query, description = "Whether the label is an edge type")
52 ),
53 responses(
54 (status = 200, body = serde_json::Value, description = "Schema version history"),
55 (status = 500, description = "Internal error")
56 )
57)]
58pub async fn get_version_history<
60 S: StorageClient
61 + StorageSchemaContextOps
62 + StorageSyncContextOps
63 + StorageOperationContextOps
64 + Clone
65 + Send
66 + Sync
67 + 'static,
68>(
69 State(state): State<AppState<S>>,
70 Path((space, label)): Path<(String, String)>,
71 axum::extract::Query(query): axum::extract::Query<std::collections::HashMap<String, String>>,
72) -> Result<JsonResponse<serde_json::Value>, HttpError> {
73 let is_edge = parse_is_edge_param(&query)?;
74
75 let result = task::spawn_blocking(move || {
76 let storage = state.server.get_storage();
77 let storage_read = storage.read();
78
79 let history = if is_edge {
80 storage_read.get_edge_version_history(&space, &label)
81 } else {
82 storage_read.get_vertex_version_history(&space, &label)
83 }
84 .map_err(|e| HttpError::InternalError(format!("Failed to get version history: {}", e)))?;
85
86 let versions = history
87 .map(|h| {
88 h.change_log
89 .get_versions()
90 .iter()
91 .map(|&version| {
92 let version_changes = h
93 .change_log
94 .get_version_changes(version)
95 .cloned()
96 .unwrap_or_default();
97 let timestamp_ms = version_changes
98 .iter()
99 .map(|c| c.timestamp_ms)
100 .max()
101 .unwrap_or(0);
102 let changes: Vec<_> = version_changes
103 .into_iter()
104 .map(|change| ChangeInfo {
105 change_type: format!("{:?}", change.details),
106 description: change.details.description(),
107 details: {
108 let mut d = std::collections::HashMap::new();
109 d.insert(
110 "description".to_string(),
111 change.details.description(),
112 );
113 d
114 },
115 })
116 .collect();
117
118 serde_json::json!({
119 "version": version,
120 "timestamp_ms": timestamp_ms,
121 "changes": changes,
122 })
123 })
124 .collect::<Vec<_>>()
125 })
126 .unwrap_or_default();
127
128 Ok::<_, HttpError>(serde_json::json!({
129 "space": space,
130 "label": label,
131 "is_edge": is_edge,
132 "versions": versions,
133 }))
134 })
135 .await
136 .map_err(|e| HttpError::InternalError(format!("Task execution failed: {}", e)))?;
137
138 Ok(JsonResponse(result?))
139}
140
141#[utoipa::path(
142 get,
143 path = "/v1/schema/changes/{space}/{label}/{from_version}/{to_version}",
144 tag = "Schema",
145 params(
146 ("space" = String, Path, description = "Space name"),
147 ("label" = String, Path, description = "Tag or edge type name"),
148 ("from_version" = u64, Path, description = "Start schema version"),
149 ("to_version" = u64, Path, description = "End schema version"),
150 ("is_edge" = Option<bool>, Query, description = "Whether the label is an edge type")
151 ),
152 responses(
153 (status = 200, body = serde_json::Value, description = "Schema changes between versions"),
154 (status = 500, description = "Internal error")
155 )
156)]
157pub async fn get_schema_changes<
159 S: StorageClient
160 + StorageSchemaContextOps
161 + StorageSyncContextOps
162 + StorageOperationContextOps
163 + Clone
164 + Send
165 + Sync
166 + 'static,
167>(
168 State(state): State<AppState<S>>,
169 Path((space, label, from_version, to_version)): Path<(String, String, u64, u64)>,
170 axum::extract::Query(query): axum::extract::Query<std::collections::HashMap<String, String>>,
171) -> Result<JsonResponse<serde_json::Value>, HttpError> {
172 let is_edge = parse_is_edge_param(&query)?;
173
174 if from_version > to_version {
176 return Err(HttpError::BadRequest(format!(
177 "Invalid version range: from_version ({}) must be <= to_version ({})",
178 from_version, to_version
179 )));
180 }
181
182 let result = task::spawn_blocking(move || {
183 let storage = state.server.get_storage();
184 let storage_read = storage.read();
185
186 let changes = if is_edge {
187 storage_read.get_edge_schema_changes(&space, &label, from_version, to_version)
188 } else {
189 storage_read.get_vertex_schema_changes(&space, &label, from_version, to_version)
190 }
191 .map_err(|e| HttpError::InternalError(format!("Failed to get schema changes: {}", e)))?;
192
193 let change_list: Vec<_> = changes
194 .iter()
195 .map(|change| {
196 let mut details_map: std::collections::HashMap<&str, String> =
197 std::collections::HashMap::new();
198 details_map.insert("description".into(), change.details.description());
199 serde_json::json!({
200 "change_type": format!("{:?}", change.details),
201 "description": change.details.description(),
202 "details": details_map,
203 })
204 })
205 .collect();
206
207 Ok::<_, HttpError>(serde_json::json!({
208 "space": space,
209 "label": label,
210 "is_edge": is_edge,
211 "from_version": from_version,
212 "to_version": to_version,
213 "changes": change_list,
214 }))
215 })
216 .await
217 .map_err(|e| HttpError::InternalError(format!("Task execution failed: {}", e)))?;
218
219 Ok(JsonResponse(result?))
220}
221
222#[utoipa::path(
223 get,
224 path = "/v1/schema/breaking-changes/{space}/{label}/{from_version}/{to_version}",
225 tag = "Schema",
226 params(
227 ("space" = String, Path, description = "Space name"),
228 ("label" = String, Path, description = "Tag or edge type name"),
229 ("from_version" = u64, Path, description = "Start schema version"),
230 ("to_version" = u64, Path, description = "End schema version"),
231 ("is_edge" = Option<bool>, Query, description = "Whether the label is an edge type")
232 ),
233 responses(
234 (status = 200, body = serde_json::Value, description = "Breaking change report"),
235 (status = 500, description = "Internal error")
236 )
237)]
238pub async fn detect_breaking_changes<
240 S: StorageClient
241 + StorageSchemaContextOps
242 + StorageSyncContextOps
243 + StorageOperationContextOps
244 + Clone
245 + Send
246 + Sync
247 + 'static,
248>(
249 State(state): State<AppState<S>>,
250 Path((space, label, from_version, to_version)): Path<(String, String, u64, u64)>,
251 axum::extract::Query(query): axum::extract::Query<std::collections::HashMap<String, String>>,
252) -> Result<JsonResponse<serde_json::Value>, HttpError> {
253 let is_edge = parse_is_edge_param(&query)?;
254
255 if from_version > to_version {
257 return Err(HttpError::BadRequest(format!(
258 "Invalid version range: from_version ({}) must be <= to_version ({})",
259 from_version, to_version
260 )));
261 }
262
263 let result = task::spawn_blocking(move || {
264 let storage = state.server.get_storage();
265 let storage_read = storage.read();
266
267 let changes = if is_edge {
268 storage_read.detect_edge_breaking_changes(&space, &label, from_version, to_version)
269 } else {
270 storage_read.detect_vertex_breaking_changes(&space, &label, from_version, to_version)
271 }
272 .map_err(|e| {
273 HttpError::InternalError(format!("Failed to detect breaking changes: {}", e))
274 })?;
275
276 let has_breaking = !changes.is_empty();
277 let change_list: Vec<_> = changes
278 .iter()
279 .map(|change| {
280 let mut details_map: std::collections::HashMap<&str, String> =
281 std::collections::HashMap::new();
282 details_map.insert("description".into(), change.details.description());
283 serde_json::json!({
284 "change_type": format!("{:?}", change.details),
285 "description": change.details.description(),
286 "details": details_map,
287 })
288 })
289 .collect();
290
291 let recommendation = if has_breaking {
292 format!(
293 "Found {} breaking changes. Data migration may be required.",
294 change_list.len()
295 )
296 } else {
297 "No breaking changes detected".to_string()
298 };
299
300 Ok::<_, HttpError>(serde_json::json!({
301 "space": space,
302 "label": label,
303 "is_edge": is_edge,
304 "from_version": from_version,
305 "to_version": to_version,
306 "has_breaking_changes": has_breaking,
307 "changes": change_list,
308 "recommendation": recommendation,
309 }))
310 })
311 .await
312 .map_err(|e| HttpError::InternalError(format!("Task execution failed: {}", e)))?;
313
314 Ok(JsonResponse(result?))
315}