-47
api/src/api/actors.rs
-47
api/src/api/actors.rs
···
1
-
use axum::{
2
-
extract::State,
3
-
http::StatusCode,
4
-
response::Json,
5
-
Json as ExtractJson,
6
-
};
7
-
use serde::{Deserialize, Serialize};
8
-
use std::collections::HashMap;
9
-
use crate::{AppState, models::{Actor, WhereCondition}};
10
-
11
-
#[derive(Deserialize)]
12
-
#[serde(rename_all = "camelCase")]
13
-
pub struct GetActorsParams {
14
-
pub slice: String,
15
-
pub limit: Option<i32>,
16
-
pub cursor: Option<String>,
17
-
#[serde(rename = "where")]
18
-
pub where_conditions: Option<HashMap<String, WhereCondition>>,
19
-
}
20
-
21
-
#[derive(Serialize)]
22
-
#[serde(rename_all = "camelCase")]
23
-
pub struct GetActorsResponse {
24
-
pub actors: Vec<Actor>,
25
-
pub cursor: Option<String>,
26
-
}
27
-
28
-
pub async fn get_actors(
29
-
State(state): State<AppState>,
30
-
ExtractJson(params): ExtractJson<GetActorsParams>,
31
-
) -> Result<Json<GetActorsResponse>, StatusCode> {
32
-
match state.database.get_slice_actors(
33
-
¶ms.slice,
34
-
params.limit,
35
-
params.cursor.as_deref(),
36
-
params.where_conditions.as_ref(),
37
-
).await {
38
-
Ok((actors, cursor)) => {
39
-
let response = GetActorsResponse {
40
-
actors,
41
-
cursor,
42
-
};
43
-
Ok(Json(response))
44
-
},
45
-
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
46
-
}
47
-
}
-34
api/src/api/jetstream.rs
-34
api/src/api/jetstream.rs
···
1
-
use axum::{
2
-
extract::State,
3
-
http::StatusCode,
4
-
response::Json as ResponseJson,
5
-
};
6
-
use serde::Serialize;
7
-
use std::sync::atomic::Ordering;
8
-
use crate::AppState;
9
-
10
-
#[derive(Serialize)]
11
-
#[serde(rename_all = "camelCase")]
12
-
pub struct JetstreamStatusResponse {
13
-
connected: bool,
14
-
status: String,
15
-
error: Option<String>,
16
-
}
17
-
18
-
pub async fn get_jetstream_status(
19
-
State(state): State<AppState>,
20
-
) -> Result<ResponseJson<JetstreamStatusResponse>, StatusCode> {
21
-
let connected = state.jetstream_connected.load(Ordering::Relaxed);
22
-
23
-
let (status, error) = if connected {
24
-
("Connected".to_string(), None)
25
-
} else {
26
-
("Disconnected".to_string(), Some("Jetstream consumer is not connected".to_string()))
27
-
};
28
-
29
-
Ok(ResponseJson(JetstreamStatusResponse {
30
-
connected,
31
-
status,
32
-
error,
33
-
}))
34
-
}
-59
api/src/api/jobs.rs
-59
api/src/api/jobs.rs
···
1
-
use axum::{
2
-
extract::{Query, State},
3
-
http::StatusCode,
4
-
response::Json,
5
-
};
6
-
use serde::Deserialize;
7
-
use uuid::Uuid;
8
-
use crate::AppState;
9
-
use crate::jobs;
10
-
11
-
/// Query parameters for getting job status
12
-
#[derive(Debug, Deserialize)]
13
-
#[serde(rename_all = "camelCase")]
14
-
pub struct GetJobStatusQuery {
15
-
pub job_id: Uuid,
16
-
}
17
-
18
-
/// Query parameters for getting slice job history
19
-
#[derive(Debug, Deserialize)]
20
-
#[serde(rename_all = "camelCase")]
21
-
pub struct GetSliceJobHistoryQuery {
22
-
pub user_did: String,
23
-
pub slice_uri: String,
24
-
pub limit: Option<i64>,
25
-
}
26
-
27
-
/// Get the status of a specific job by ID (XRPC style)
28
-
pub async fn get_job_status(
29
-
State(state): State<AppState>,
30
-
Query(query): Query<GetJobStatusQuery>,
31
-
) -> Result<Json<jobs::JobStatus>, StatusCode> {
32
-
match jobs::get_job_status(&state.database_pool, query.job_id).await {
33
-
Ok(Some(status)) => Ok(Json(status)),
34
-
Ok(None) => Err(StatusCode::NOT_FOUND),
35
-
Err(e) => {
36
-
tracing::error!("Failed to get job status: {}", e);
37
-
Err(StatusCode::INTERNAL_SERVER_ERROR)
38
-
}
39
-
}
40
-
}
41
-
42
-
/// Get job history for a specific slice (XRPC style)
43
-
pub async fn get_slice_job_history(
44
-
State(state): State<AppState>,
45
-
Query(query): Query<GetSliceJobHistoryQuery>,
46
-
) -> Result<Json<Vec<jobs::JobStatus>>, StatusCode> {
47
-
match jobs::get_slice_job_history(
48
-
&state.database_pool,
49
-
&query.user_did,
50
-
&query.slice_uri,
51
-
query.limit
52
-
).await {
53
-
Ok(history) => Ok(Json(history)),
54
-
Err(e) => {
55
-
tracing::error!("Failed to get slice job history: {}", e);
56
-
Err(StatusCode::INTERNAL_SERVER_ERROR)
57
-
}
58
-
}
59
-
}
-65
api/src/api/logs.rs
-65
api/src/api/logs.rs
···
1
-
use axum::{
2
-
extract::{Query, State},
3
-
http::StatusCode,
4
-
response::Json,
5
-
};
6
-
use serde::{Deserialize, Serialize};
7
-
use uuid::Uuid;
8
-
9
-
use crate::{AppState, logging::{get_sync_job_logs, get_jetstream_logs, LogEntry}};
10
-
11
-
#[derive(Debug, Deserialize)]
12
-
pub struct LogsQuery {
13
-
pub limit: Option<i64>,
14
-
pub slice: Option<String>,
15
-
}
16
-
17
-
#[derive(Debug, Serialize)]
18
-
pub struct LogsResponse {
19
-
pub logs: Vec<LogEntry>,
20
-
}
21
-
22
-
#[derive(Debug, Deserialize)]
23
-
#[serde(rename_all = "camelCase")]
24
-
pub struct LogsQueryWithJobId {
25
-
pub job_id: Uuid,
26
-
pub limit: Option<i64>,
27
-
}
28
-
29
-
/// Get logs for a specific sync job
30
-
pub async fn get_sync_job_logs_handler(
31
-
State(state): State<AppState>,
32
-
Query(params): Query<LogsQueryWithJobId>,
33
-
) -> Result<Json<LogsResponse>, StatusCode> {
34
-
match get_sync_job_logs(&state.database_pool, params.job_id, params.limit).await {
35
-
Ok(logs) => Ok(Json(LogsResponse { logs })),
36
-
Err(e) => {
37
-
tracing::error!("Failed to get sync job logs: {}", e);
38
-
Err(StatusCode::INTERNAL_SERVER_ERROR)
39
-
}
40
-
}
41
-
}
42
-
43
-
/// Get jetstream logs
44
-
pub async fn get_jetstream_logs_handler(
45
-
State(state): State<AppState>,
46
-
Query(params): Query<LogsQuery>,
47
-
) -> Result<Json<LogsResponse>, StatusCode> {
48
-
// Debug logging to see what slice filter is being used
49
-
if let Some(slice) = ¶ms.slice {
50
-
tracing::info!("Filtering jetstream logs by slice: {}", slice);
51
-
} else {
52
-
tracing::info!("No slice filter applied - returning all jetstream logs");
53
-
}
54
-
55
-
match get_jetstream_logs(&state.database_pool, params.slice.as_deref(), params.limit).await {
56
-
Ok(logs) => {
57
-
tracing::info!("Returning {} jetstream logs", logs.len());
58
-
Ok(Json(LogsResponse { logs }))
59
-
},
60
-
Err(e) => {
61
-
tracing::error!("Failed to get jetstream logs: {}", e);
62
-
Err(StatusCode::INTERNAL_SERVER_ERROR)
63
-
}
64
-
}
65
-
}
-10
api/src/api/mod.rs
-10
api/src/api/mod.rs
-524
api/src/api/oauth.rs
-524
api/src/api/oauth.rs
···
1
-
use axum::{
2
-
extract::{Query, State},
3
-
http::HeaderMap,
4
-
response::Json,
5
-
Json as ExtractJson,
6
-
};
7
-
use serde::{Deserialize, Serialize};
8
-
use reqwest::Client;
9
-
10
-
use crate::{
11
-
AppState,
12
-
auth,
13
-
errors::AppError,
14
-
models::{
15
-
CreateOAuthClientRequest, OAuthClientDetails, ListOAuthClientsResponse,
16
-
UpdateOAuthClientRequest, DeleteOAuthClientResponse
17
-
},
18
-
};
19
-
20
-
#[derive(Deserialize)]
21
-
#[serde(rename_all = "camelCase")]
22
-
pub struct GetOAuthClientsQuery {
23
-
pub slice: String,
24
-
}
25
-
26
-
#[derive(Deserialize)]
27
-
#[serde(rename_all = "camelCase")]
28
-
pub struct DeleteOAuthClientRequest {
29
-
pub client_id: String,
30
-
}
31
-
32
-
#[derive(Debug, Serialize, Deserialize)]
33
-
#[serde(rename_all = "snake_case")]
34
-
struct AipClientRegistrationRequest {
35
-
pub client_name: String,
36
-
pub redirect_uris: Vec<String>,
37
-
#[serde(skip_serializing_if = "Option::is_none")]
38
-
pub grant_types: Option<Vec<String>>,
39
-
#[serde(skip_serializing_if = "Option::is_none")]
40
-
pub response_types: Option<Vec<String>>,
41
-
#[serde(skip_serializing_if = "Option::is_none")]
42
-
pub scope: Option<String>,
43
-
#[serde(skip_serializing_if = "Option::is_none")]
44
-
pub client_uri: Option<String>,
45
-
#[serde(skip_serializing_if = "Option::is_none")]
46
-
pub logo_uri: Option<String>,
47
-
#[serde(skip_serializing_if = "Option::is_none")]
48
-
pub tos_uri: Option<String>,
49
-
#[serde(skip_serializing_if = "Option::is_none")]
50
-
pub policy_uri: Option<String>,
51
-
}
52
-
53
-
#[derive(Serialize, Deserialize)]
54
-
#[serde(rename_all = "snake_case")]
55
-
struct AipClientRegistrationResponse {
56
-
pub client_id: String,
57
-
#[serde(skip_serializing_if = "Option::is_none")]
58
-
pub client_secret: Option<String>,
59
-
#[serde(skip_serializing_if = "Option::is_none")]
60
-
pub registration_access_token: Option<String>,
61
-
pub client_name: String,
62
-
pub redirect_uris: Vec<String>,
63
-
pub grant_types: Vec<String>,
64
-
pub response_types: Vec<String>,
65
-
#[serde(skip_serializing_if = "Option::is_none")]
66
-
pub scope: Option<String>,
67
-
#[serde(skip_serializing_if = "Option::is_none")]
68
-
pub client_uri: Option<String>,
69
-
#[serde(skip_serializing_if = "Option::is_none")]
70
-
pub logo_uri: Option<String>,
71
-
#[serde(skip_serializing_if = "Option::is_none")]
72
-
pub tos_uri: Option<String>,
73
-
#[serde(skip_serializing_if = "Option::is_none")]
74
-
pub policy_uri: Option<String>,
75
-
// Additional fields that AIP returns but we don't need to send
76
-
#[serde(skip_serializing_if = "Option::is_none")]
77
-
pub token_endpoint_auth_method: Option<String>,
78
-
#[serde(skip_serializing_if = "Option::is_none")]
79
-
pub registration_client_uri: Option<String>,
80
-
#[serde(skip_serializing_if = "Option::is_none")]
81
-
pub client_id_issued_at: Option<i64>,
82
-
#[serde(skip_serializing_if = "Option::is_none")]
83
-
pub client_secret_expires_at: Option<i64>,
84
-
}
85
-
86
-
pub async fn create_oauth_client(
87
-
State(state): State<AppState>,
88
-
headers: HeaderMap,
89
-
ExtractJson(request): ExtractJson<CreateOAuthClientRequest>,
90
-
) -> Result<Json<serde_json::Value>, AppError> {
91
-
// Debug log the incoming request
92
-
tracing::debug!("Incoming OAuth client registration request: {:?}", request);
93
-
94
-
// Validate request
95
-
if request.client_name.trim().is_empty() {
96
-
return Err(AppError::BadRequest("Client name cannot be empty".to_string()));
97
-
}
98
-
99
-
if request.redirect_uris.is_empty() {
100
-
return Err(AppError::BadRequest("At least one redirect URI is required".to_string()));
101
-
}
102
-
103
-
// Validate redirect URIs have basic URL format
104
-
for uri in &request.redirect_uris {
105
-
if !uri.starts_with("http://") && !uri.starts_with("https://") {
106
-
return Err(AppError::BadRequest(format!("Redirect URI must use HTTP or HTTPS: {}", uri)));
107
-
}
108
-
if uri.trim().is_empty() {
109
-
return Err(AppError::BadRequest("Redirect URI cannot be empty".to_string()));
110
-
}
111
-
}
112
-
113
-
// Extract and verify authentication
114
-
let token = auth::extract_bearer_token(&headers)
115
-
.map_err(|_| AppError::BadRequest("Missing or invalid Authorization header".to_string()))?;
116
-
let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await
117
-
.map_err(|_| AppError::BadRequest("Invalid or expired access token".to_string()))?;
118
-
119
-
let user_did = user_info.sub;
120
-
121
-
// Register client with AIP
122
-
let aip_base_url = &state.config.auth_base_url;
123
-
124
-
let client = Client::new();
125
-
let aip_request = AipClientRegistrationRequest {
126
-
client_name: request.client_name.clone(),
127
-
redirect_uris: request.redirect_uris.clone(),
128
-
grant_types: request.grant_types.clone(),
129
-
response_types: request.response_types.clone(),
130
-
scope: request.scope.clone(),
131
-
client_uri: request.client_uri.clone(),
132
-
logo_uri: request.logo_uri.clone(),
133
-
tos_uri: request.tos_uri.clone(),
134
-
policy_uri: request.policy_uri.clone(),
135
-
};
136
-
137
-
let registration_url = format!("{}/oauth/clients/register", aip_base_url);
138
-
tracing::debug!("Attempting to register OAuth client at: {}", registration_url);
139
-
tracing::debug!("Sending AIP request: {:?}", aip_request);
140
-
141
-
let aip_response = client
142
-
.post(®istration_url)
143
-
.json(&aip_request)
144
-
.send()
145
-
.await
146
-
.map_err(|e| AppError::Internal(format!("Failed to register client with AIP: {}", e)))?;
147
-
148
-
let status = aip_response.status();
149
-
tracing::debug!("AIP registration response status: {}", status);
150
-
151
-
if !status.is_success() {
152
-
let error_text = aip_response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
153
-
tracing::error!("AIP registration failed with status {}: {}", status, error_text);
154
-
155
-
// Try to parse the error response as JSON to get more details
156
-
let detailed_error = if let Ok(error_json) = serde_json::from_str::<serde_json::Value>(&error_text) {
157
-
if let Some(error_desc) = error_json.get("error_description").and_then(|v| v.as_str()) {
158
-
error_desc.to_string()
159
-
} else if let Some(error) = error_json.get("error").and_then(|v| v.as_str()) {
160
-
error.to_string()
161
-
} else {
162
-
error_text
163
-
}
164
-
} else {
165
-
error_text
166
-
};
167
-
168
-
// Return success=false with detailed error message instead of HTTP error
169
-
return Ok(Json(serde_json::json!({
170
-
"success": false,
171
-
"message": detailed_error
172
-
})));
173
-
}
174
-
175
-
tracing::debug!("Parsing AIP response JSON...");
176
-
177
-
// Get the response body as text first to debug what we're receiving
178
-
let response_body = aip_response
179
-
.text()
180
-
.await
181
-
.map_err(|e| AppError::Internal(format!("Failed to get response body: {}", e)))?;
182
-
183
-
tracing::debug!("AIP response body: {}", response_body);
184
-
185
-
// Try to parse the JSON from the text
186
-
let aip_client: AipClientRegistrationResponse = serde_json::from_str(&response_body)
187
-
.map_err(|e| {
188
-
tracing::error!("Failed to parse AIP response JSON: {}", e);
189
-
tracing::error!("Raw response was: {}", response_body);
190
-
AppError::Internal(format!("Failed to parse AIP response: {}", e))
191
-
})?;
192
-
193
-
tracing::debug!("Successfully parsed AIP response, client_id: {}", aip_client.client_id);
194
-
195
-
// Store the client info in our database
196
-
tracing::debug!("Storing OAuth client in database...");
197
-
let oauth_client = state.database
198
-
.create_oauth_client(
199
-
&request.slice_uri,
200
-
&aip_client.client_id,
201
-
aip_client.registration_access_token.as_deref(),
202
-
&user_did,
203
-
)
204
-
.await
205
-
.map_err(|e| {
206
-
tracing::error!("Failed to store OAuth client in database: {}", e);
207
-
AppError::Internal(format!("Failed to store OAuth client: {}", e))
208
-
})?;
209
-
210
-
tracing::debug!("Successfully stored OAuth client in database");
211
-
212
-
// Return the full client details from AIP
213
-
let response = OAuthClientDetails {
214
-
client_id: aip_client.client_id,
215
-
client_secret: aip_client.client_secret,
216
-
client_name: aip_client.client_name,
217
-
redirect_uris: aip_client.redirect_uris,
218
-
grant_types: aip_client.grant_types,
219
-
response_types: aip_client.response_types,
220
-
scope: aip_client.scope,
221
-
client_uri: aip_client.client_uri,
222
-
logo_uri: aip_client.logo_uri,
223
-
tos_uri: aip_client.tos_uri,
224
-
policy_uri: aip_client.policy_uri,
225
-
created_at: oauth_client.created_at,
226
-
created_by_did: oauth_client.created_by_did,
227
-
};
228
-
229
-
Ok(Json(serde_json::to_value(response).unwrap()))
230
-
}
231
-
232
-
pub async fn get_oauth_clients(
233
-
State(state): State<AppState>,
234
-
headers: HeaderMap,
235
-
Query(params): Query<GetOAuthClientsQuery>,
236
-
) -> Result<Json<ListOAuthClientsResponse>, AppError> {
237
-
tracing::debug!("get_oauth_clients called with slice parameter: {}", params.slice);
238
-
239
-
// Log all headers for debugging
240
-
tracing::debug!("Request headers: {:?}", headers);
241
-
242
-
// Extract and verify authentication
243
-
let token = auth::extract_bearer_token(&headers)
244
-
.map_err(|e| {
245
-
tracing::error!("Failed to extract bearer token: {:?}", e);
246
-
AppError::BadRequest("Missing or invalid Authorization header".to_string())
247
-
})?;
248
-
249
-
tracing::debug!("Extracted bearer token (first 20 chars): {}...",
250
-
if token.len() > 20 { &token[..20] } else { &token });
251
-
252
-
auth::verify_oauth_token(&token, &state.config.auth_base_url).await
253
-
.map_err(|e| {
254
-
tracing::error!("OAuth token verification failed: {:?}", e);
255
-
AppError::BadRequest("Invalid or expired access token".to_string())
256
-
})?;
257
-
258
-
tracing::debug!("OAuth token verification successful");
259
-
260
-
// Get clients from our database
261
-
let clients = state.database
262
-
.get_oauth_clients_for_slice(¶ms.slice)
263
-
.await
264
-
.map_err(|e| AppError::Internal(format!("Failed to fetch OAuth clients: {}", e)))?;
265
-
266
-
tracing::debug!("Found {} OAuth clients in database for slice: {}", clients.len(), params.slice);
267
-
268
-
if clients.is_empty() {
269
-
return Ok(Json(ListOAuthClientsResponse { clients: vec![] }));
270
-
}
271
-
272
-
// Fetch detailed info from AIP for each client
273
-
let aip_base_url = &state.config.auth_base_url;
274
-
let client = Client::new();
275
-
let mut client_details = Vec::new();
276
-
277
-
for oauth_client in clients {
278
-
// Fetch client details from AIP
279
-
let aip_url = format!("{}/oauth/clients/{}", aip_base_url, oauth_client.client_id);
280
-
tracing::debug!("Fetching client details from AIP: {}", aip_url);
281
-
282
-
// Use registration access token if available for authentication
283
-
let mut request_builder = client.get(&aip_url);
284
-
if let Some(token) = &oauth_client.registration_access_token {
285
-
request_builder = request_builder.bearer_auth(token);
286
-
tracing::debug!("Using registration access token for authentication");
287
-
} else {
288
-
tracing::debug!("No registration access token available");
289
-
}
290
-
291
-
let aip_response = request_builder.send().await;
292
-
293
-
match aip_response {
294
-
Ok(response) => {
295
-
let status = response.status();
296
-
tracing::debug!("AIP response status for {}: {}", oauth_client.client_id, status);
297
-
298
-
if status.is_success() {
299
-
// Get the response body as text first to log it
300
-
match response.text().await {
301
-
Ok(response_text) => {
302
-
tracing::debug!("AIP response body for {}: {}", oauth_client.client_id, response_text);
303
-
304
-
// Try to parse the JSON
305
-
match serde_json::from_str::<AipClientRegistrationResponse>(&response_text) {
306
-
Ok(aip_client) => {
307
-
tracing::debug!("Successfully parsed AIP client details for {}", oauth_client.client_id);
308
-
client_details.push(OAuthClientDetails {
309
-
client_id: aip_client.client_id,
310
-
client_secret: aip_client.client_secret,
311
-
client_name: aip_client.client_name,
312
-
redirect_uris: aip_client.redirect_uris,
313
-
grant_types: aip_client.grant_types,
314
-
response_types: aip_client.response_types,
315
-
scope: aip_client.scope,
316
-
client_uri: aip_client.client_uri,
317
-
logo_uri: aip_client.logo_uri,
318
-
tos_uri: aip_client.tos_uri,
319
-
policy_uri: aip_client.policy_uri,
320
-
created_at: oauth_client.created_at,
321
-
created_by_did: oauth_client.created_by_did,
322
-
});
323
-
}
324
-
Err(parse_error) => {
325
-
tracing::error!("Failed to parse AIP client JSON for {}: {}", oauth_client.client_id, parse_error);
326
-
}
327
-
}
328
-
}
329
-
Err(text_error) => {
330
-
tracing::error!("Failed to get AIP response text for {}: {}", oauth_client.client_id, text_error);
331
-
}
332
-
}
333
-
} else {
334
-
// Handle non-success status codes
335
-
match response.text().await {
336
-
Ok(error_text) => {
337
-
tracing::error!("AIP client fetch failed with status {} for {}: {}", status, oauth_client.client_id, error_text);
338
-
}
339
-
Err(_) => {
340
-
tracing::error!("AIP client fetch failed with status {} for {}", status, oauth_client.client_id);
341
-
}
342
-
}
343
-
}
344
-
}
345
-
Err(e) => {
346
-
tracing::error!("AIP client fetch error for {}: {}", oauth_client.client_id, e);
347
-
// If we can't fetch from AIP, create a minimal response
348
-
client_details.push(OAuthClientDetails {
349
-
client_id: oauth_client.client_id.clone(),
350
-
client_secret: None,
351
-
client_name: "Unknown".to_string(),
352
-
redirect_uris: vec![],
353
-
grant_types: vec!["authorization_code".to_string()],
354
-
response_types: vec!["code".to_string()],
355
-
scope: None,
356
-
client_uri: None,
357
-
logo_uri: None,
358
-
tos_uri: None,
359
-
policy_uri: None,
360
-
created_at: oauth_client.created_at,
361
-
created_by_did: oauth_client.created_by_did,
362
-
});
363
-
}
364
-
}
365
-
}
366
-
367
-
Ok(Json(ListOAuthClientsResponse { clients: client_details }))
368
-
}
369
-
370
-
pub async fn update_oauth_client(
371
-
State(state): State<AppState>,
372
-
headers: HeaderMap,
373
-
ExtractJson(request): ExtractJson<UpdateOAuthClientRequest>,
374
-
) -> Result<Json<serde_json::Value>, AppError> {
375
-
let client_id = request.client_id.clone();
376
-
377
-
// Extract and verify authentication
378
-
let token = auth::extract_bearer_token(&headers)
379
-
.map_err(|_| AppError::BadRequest("Missing or invalid Authorization header".to_string()))?;
380
-
auth::verify_oauth_token(&token, &state.config.auth_base_url).await
381
-
.map_err(|_| AppError::BadRequest("Invalid or expired access token".to_string()))?;
382
-
383
-
// Get the client from our database to get the registration access token
384
-
let oauth_client = state.database
385
-
.get_oauth_client_by_id(&client_id)
386
-
.await
387
-
.map_err(|e| AppError::Internal(format!("Failed to fetch OAuth client: {}", e)))?
388
-
.ok_or_else(|| AppError::NotFound("OAuth client not found".to_string()))?;
389
-
390
-
let registration_token = oauth_client.registration_access_token
391
-
.ok_or_else(|| AppError::Internal("Client missing registration access token".to_string()))?;
392
-
393
-
// Build AIP update request
394
-
let aip_request = AipClientRegistrationRequest {
395
-
client_name: request.client_name.unwrap_or_default(),
396
-
redirect_uris: request.redirect_uris.unwrap_or_default(),
397
-
grant_types: None, // Keep existing
398
-
response_types: None, // Keep existing
399
-
scope: request.scope,
400
-
client_uri: request.client_uri,
401
-
logo_uri: request.logo_uri,
402
-
tos_uri: request.tos_uri,
403
-
policy_uri: request.policy_uri,
404
-
};
405
-
406
-
let aip_base_url = &state.config.auth_base_url;
407
-
let client = Client::new();
408
-
let update_url = format!("{}/oauth/clients/{}", aip_base_url, client_id);
409
-
410
-
tracing::debug!("Updating OAuth client at: {}", update_url);
411
-
tracing::debug!("Sending AIP update request: {:?}", aip_request);
412
-
413
-
let aip_response = client
414
-
.put(&update_url)
415
-
.bearer_auth(®istration_token)
416
-
.json(&aip_request)
417
-
.send()
418
-
.await
419
-
.map_err(|e| AppError::Internal(format!("Failed to update client with AIP: {}", e)))?;
420
-
421
-
let status = aip_response.status();
422
-
tracing::debug!("AIP update response status: {}", status);
423
-
424
-
if !status.is_success() {
425
-
let error_text = aip_response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
426
-
tracing::error!("AIP update failed with status {}: {}", status, error_text);
427
-
428
-
// Try to parse the error response as JSON to get more details
429
-
let detailed_error = if let Ok(error_json) = serde_json::from_str::<serde_json::Value>(&error_text) {
430
-
if let Some(error_desc) = error_json.get("error_description").and_then(|v| v.as_str()) {
431
-
error_desc.to_string()
432
-
} else if let Some(error) = error_json.get("error").and_then(|v| v.as_str()) {
433
-
error.to_string()
434
-
} else {
435
-
error_text
436
-
}
437
-
} else {
438
-
error_text
439
-
};
440
-
441
-
// Return success=false with detailed error message instead of HTTP error
442
-
return Ok(Json(serde_json::json!({
443
-
"success": false,
444
-
"message": detailed_error
445
-
})));
446
-
}
447
-
448
-
// Parse the response
449
-
let response_body = aip_response
450
-
.text()
451
-
.await
452
-
.map_err(|e| AppError::Internal(format!("Failed to get response body: {}", e)))?;
453
-
454
-
tracing::debug!("AIP update response body: {}", response_body);
455
-
456
-
let aip_client: AipClientRegistrationResponse = serde_json::from_str(&response_body)
457
-
.map_err(|e| {
458
-
tracing::error!("Failed to parse AIP response JSON: {}", e);
459
-
AppError::Internal(format!("Failed to parse AIP response: {}", e))
460
-
})?;
461
-
462
-
// Return the updated client details
463
-
let response = OAuthClientDetails {
464
-
client_id: aip_client.client_id,
465
-
client_secret: aip_client.client_secret,
466
-
client_name: aip_client.client_name,
467
-
redirect_uris: aip_client.redirect_uris,
468
-
grant_types: aip_client.grant_types,
469
-
response_types: aip_client.response_types,
470
-
scope: aip_client.scope,
471
-
client_uri: aip_client.client_uri,
472
-
logo_uri: aip_client.logo_uri,
473
-
tos_uri: aip_client.tos_uri,
474
-
policy_uri: aip_client.policy_uri,
475
-
created_at: oauth_client.created_at,
476
-
created_by_did: oauth_client.created_by_did,
477
-
};
478
-
479
-
Ok(Json(serde_json::to_value(response).unwrap()))
480
-
}
481
-
482
-
pub async fn delete_oauth_client(
483
-
State(state): State<AppState>,
484
-
headers: HeaderMap,
485
-
ExtractJson(request): ExtractJson<DeleteOAuthClientRequest>,
486
-
) -> Result<Json<DeleteOAuthClientResponse>, AppError> {
487
-
let client_id = request.client_id;
488
-
// Extract and verify authentication
489
-
let token = auth::extract_bearer_token(&headers)
490
-
.map_err(|_| AppError::BadRequest("Missing or invalid Authorization header".to_string()))?;
491
-
auth::verify_oauth_token(&token, &state.config.auth_base_url).await
492
-
.map_err(|_| AppError::BadRequest("Invalid or expired access token".to_string()))?;
493
-
494
-
// Get the client from our database first
495
-
let oauth_client = state.database
496
-
.get_oauth_client_by_id(&client_id)
497
-
.await
498
-
.map_err(|e| AppError::Internal(format!("Failed to fetch OAuth client: {}", e)))?
499
-
.ok_or_else(|| AppError::NotFound("OAuth client not found".to_string()))?;
500
-
501
-
// Delete from AIP if we have a registration access token
502
-
if let Some(registration_token) = &oauth_client.registration_access_token {
503
-
let aip_base_url = &state.config.auth_base_url;
504
-
505
-
let client = Client::new();
506
-
let _aip_response = client
507
-
.delete(&format!("{}/oauth/clients/{}", aip_base_url, client_id))
508
-
.bearer_auth(registration_token)
509
-
.send()
510
-
.await;
511
-
// We continue even if AIP deletion fails, as we want to clean up our database
512
-
}
513
-
514
-
// Delete from our database
515
-
state.database
516
-
.delete_oauth_client(&client_id)
517
-
.await
518
-
.map_err(|e| AppError::Internal(format!("Failed to delete OAuth client: {}", e)))?;
519
-
520
-
Ok(Json(DeleteOAuthClientResponse {
521
-
success: true,
522
-
message: format!("OAuth client {} deleted successfully", client_id),
523
-
}))
524
-
}
+14
-24
api/src/api/openapi.rs
+14
-24
api/src/api/openapi.rs
···
787
787
fn create_record_schema_from_lexicon(lexicon_data: Option<&serde_json::Value>, slice_uri: &str) -> OpenApiSchema {
788
788
if let Some(lexicon) = lexicon_data {
789
789
// Get the definitions object directly (it's already parsed JSON, not a string)
790
-
if let Some(definitions) = lexicon.get("defs") {
791
-
if let Some(main_def) = definitions.get("main") {
792
-
if let Some(record_def) = main_def.get("record") {
793
-
if let Some(properties) = record_def.get("properties") {
790
+
if let Some(definitions) = lexicon.get("defs")
791
+
&& let Some(main_def) = definitions.get("main")
792
+
&& let Some(record_def) = main_def.get("record")
793
+
&& let Some(properties) = record_def.get("properties") {
794
794
// Convert lexicon properties to OpenAPI schema properties
795
795
let mut openapi_props = HashMap::new();
796
796
let mut required_fields = Vec::new();
797
797
798
798
// Get required fields from record level
799
-
if let Some(required_array) = record_def.get("required") {
800
-
if let Some(required_list) = required_array.as_array() {
799
+
if let Some(required_array) = record_def.get("required")
800
+
&& let Some(required_list) = required_array.as_array() {
801
801
for req_field in required_list {
802
802
if let Some(field_name) = req_field.as_str() {
803
803
required_fields.push(field_name.to_string());
804
804
}
805
805
}
806
806
}
807
-
}
808
807
809
808
let default_example = if let Some(props_obj) = properties.as_object() {
810
809
for (prop_name, prop_def) in props_obj {
···
839
838
default: default_example,
840
839
};
841
840
}
842
-
}
843
-
}
844
-
}
845
841
}
846
842
847
843
// Fallback to generic object schema (without rkey - that's a separate request parameter)
···
938
934
default: None,
939
935
}),
940
936
"array" => {
941
-
if let Some(items_def) = prop_def.get("items") {
942
-
if let Some(items_schema) = convert_lexicon_property_to_openapi(items_def) {
937
+
if let Some(items_def) = prop_def.get("items")
938
+
&& let Some(items_schema) = convert_lexicon_property_to_openapi(items_def) {
943
939
return Some(OpenApiSchema {
944
940
schema_type: "array".to_string(),
945
941
format: None,
···
949
945
default: None,
950
946
});
951
947
}
952
-
}
953
948
Some(OpenApiSchema {
954
949
schema_type: "array".to_string(),
955
950
format: None,
···
1130
1125
where_conditions.insert("indexedAt".to_string(), where_condition_schema.clone());
1131
1126
1132
1127
// Extract fields from lexicon if available
1133
-
if let Some(lexicon) = lexicon_data {
1134
-
if let Some(definitions) = lexicon.get("definitions").or_else(|| lexicon.get("defs")) {
1135
-
if let Some(main_def) = definitions.get("main") {
1136
-
if let Some(record_def) = main_def.get("record") {
1137
-
if let Some(record_properties) = record_def.get("properties") {
1138
-
if let Some(props_obj) = record_properties.as_object() {
1128
+
if let Some(lexicon) = lexicon_data
1129
+
&& let Some(definitions) = lexicon.get("definitions").or_else(|| lexicon.get("defs"))
1130
+
&& let Some(main_def) = definitions.get("main")
1131
+
&& let Some(record_def) = main_def.get("record")
1132
+
&& let Some(record_properties) = record_def.get("properties")
1133
+
&& let Some(props_obj) = record_properties.as_object() {
1139
1134
for field_name in props_obj.keys() {
1140
1135
where_conditions.insert(field_name.clone(), where_condition_schema.clone());
1141
1136
}
1142
1137
}
1143
-
}
1144
-
}
1145
-
}
1146
-
}
1147
-
}
1148
1138
1149
1139
properties.insert("where".to_string(), OpenApiSchema {
1150
1140
schema_type: "object".to_string(),
-49
api/src/api/records.rs
-49
api/src/api/records.rs
···
1
-
use axum::{
2
-
extract::State,
3
-
http::StatusCode,
4
-
response::Json,
5
-
};
6
-
use crate::models::{SliceRecordsParams, SliceRecordsOutput, IndexedRecord};
7
-
use crate::AppState;
8
-
9
-
pub async fn get_records(
10
-
State(state): State<AppState>,
11
-
axum::extract::Json(params): axum::extract::Json<SliceRecordsParams>,
12
-
) -> Result<Json<SliceRecordsOutput>, StatusCode> {
13
-
match get_slice_records(&state, ¶ms).await {
14
-
Ok(output) => Ok(Json(output)),
15
-
Err(_) => Ok(Json(SliceRecordsOutput {
16
-
success: false,
17
-
records: vec![],
18
-
cursor: None,
19
-
message: Some("Failed to get slice records".to_string()),
20
-
})),
21
-
}
22
-
}
23
-
24
-
async fn get_slice_records(state: &AppState, params: &SliceRecordsParams) -> Result<SliceRecordsOutput, Box<dyn std::error::Error + Send + Sync>> {
25
-
let (records, cursor) = state.database.get_slice_collections_records(
26
-
¶ms.slice,
27
-
params.limit,
28
-
params.cursor.as_deref(),
29
-
params.sort_by.as_ref(),
30
-
params.where_clause.as_ref(),
31
-
).await?;
32
-
33
-
// Transform Record to IndexedRecord for the response
34
-
let indexed_records: Vec<IndexedRecord> = records.into_iter().map(|record| IndexedRecord {
35
-
uri: record.uri,
36
-
cid: record.cid,
37
-
did: record.did,
38
-
collection: record.collection,
39
-
value: record.json,
40
-
indexed_at: record.indexed_at.to_rfc3339(),
41
-
}).collect();
42
-
43
-
Ok(SliceRecordsOutput {
44
-
success: true,
45
-
records: indexed_records,
46
-
cursor,
47
-
message: None,
48
-
})
49
-
}
-50
api/src/api/sparkline.rs
-50
api/src/api/sparkline.rs
···
1
-
use axum::{
2
-
extract::State,
3
-
http::StatusCode,
4
-
response::Json,
5
-
};
6
-
use crate::models::{GetSparklinesParams, GetSparklinesOutput};
7
-
use crate::AppState;
8
-
9
-
pub async fn batch_sparkline(
10
-
State(state): State<AppState>,
11
-
axum::extract::Json(params): axum::extract::Json<GetSparklinesParams>,
12
-
) -> Result<Json<GetSparklinesOutput>, StatusCode> {
13
-
match get_batch_sparkline_data(&state, ¶ms).await {
14
-
Ok(output) => Ok(Json(output)),
15
-
Err(_) => Ok(Json(GetSparklinesOutput {
16
-
success: false,
17
-
sparklines: std::collections::HashMap::new(),
18
-
message: Some("Failed to get batch sparkline data".to_string()),
19
-
})),
20
-
}
21
-
}
22
-
23
-
async fn get_batch_sparkline_data(
24
-
state: &AppState,
25
-
params: &GetSparklinesParams,
26
-
) -> Result<GetSparklinesOutput, Box<dyn std::error::Error + Send + Sync>> {
27
-
let interval = params.interval.as_deref().unwrap_or("hour");
28
-
let duration = params.duration.as_deref().unwrap_or("24h");
29
-
30
-
// Parse duration
31
-
let duration_hours = match duration {
32
-
"1h" => 1,
33
-
"24h" => 24,
34
-
"7d" => 24 * 7,
35
-
"30d" => 24 * 30,
36
-
_ => 24, // default to 24h
37
-
};
38
-
39
-
let sparklines = state.database.get_batch_sparkline_data(
40
-
¶ms.slices,
41
-
interval,
42
-
duration_hours,
43
-
).await?;
44
-
45
-
Ok(GetSparklinesOutput {
46
-
success: true,
47
-
sparklines,
48
-
message: None,
49
-
})
50
-
}
-46
api/src/api/stats.rs
-46
api/src/api/stats.rs
···
1
-
use axum::{
2
-
extract::State,
3
-
http::StatusCode,
4
-
response::Json,
5
-
};
6
-
use crate::models::{SliceStatsOutput, SliceStatsParams};
7
-
use crate::AppState;
8
-
9
-
pub async fn stats(
10
-
State(state): State<AppState>,
11
-
axum::extract::Json(params): axum::extract::Json<SliceStatsParams>,
12
-
) -> Result<Json<SliceStatsOutput>, StatusCode> {
13
-
match get_slice_stats(&state, ¶ms.slice).await {
14
-
Ok(stats) => Ok(Json(stats)),
15
-
Err(_) => Ok(Json(SliceStatsOutput {
16
-
success: false,
17
-
collections: vec![],
18
-
collection_stats: vec![],
19
-
total_lexicons: 0,
20
-
total_records: 0,
21
-
total_actors: 0,
22
-
message: Some("Failed to get slice statistics".to_string()),
23
-
})),
24
-
}
25
-
}
26
-
27
-
async fn get_slice_stats(state: &AppState, slice_uri: &str) -> Result<SliceStatsOutput, Box<dyn std::error::Error + Send + Sync>> {
28
-
// Get all the slice-specific data in parallel
29
-
let (collections, collection_stats, total_lexicons, total_records, total_actors) = tokio::try_join!(
30
-
state.database.get_slice_collections_list(slice_uri),
31
-
state.database.get_slice_collection_stats(slice_uri),
32
-
state.database.get_slice_lexicon_count(slice_uri),
33
-
state.database.get_slice_total_records(slice_uri),
34
-
state.database.get_slice_total_actors(slice_uri),
35
-
)?;
36
-
37
-
Ok(SliceStatsOutput {
38
-
success: true,
39
-
collections,
40
-
collection_stats,
41
-
total_lexicons,
42
-
total_records,
43
-
total_actors,
44
-
message: None,
45
-
})
46
-
}
-146
api/src/api/sync.rs
-146
api/src/api/sync.rs
···
1
-
use crate::AppState;
2
-
use crate::auth;
3
-
use crate::jobs;
4
-
use crate::models::BulkSyncParams;
5
-
use axum::{
6
-
extract::State,
7
-
http::{HeaderMap, StatusCode},
8
-
response::Json,
9
-
};
10
-
use serde::{Deserialize, Serialize};
11
-
use tracing::{info, warn};
12
-
use uuid::Uuid;
13
-
14
-
#[derive(Debug, Deserialize)]
15
-
#[serde(rename_all = "camelCase")]
16
-
pub struct SyncRequest {
17
-
#[serde(flatten)]
18
-
pub params: BulkSyncParams,
19
-
pub slice: String,
20
-
}
21
-
22
-
#[derive(Debug, Serialize)]
23
-
#[serde(rename_all = "camelCase")]
24
-
pub struct SyncJobResponse {
25
-
pub success: bool,
26
-
pub job_id: Option<Uuid>,
27
-
pub message: String,
28
-
}
29
-
30
-
pub async fn sync(
31
-
State(state): State<AppState>,
32
-
headers: HeaderMap,
33
-
axum::extract::Json(request): axum::extract::Json<SyncRequest>,
34
-
) -> Result<Json<SyncJobResponse>, StatusCode> {
35
-
let token = auth::extract_bearer_token(&headers)?;
36
-
let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
37
-
38
-
let user_did = user_info.sub;
39
-
let slice_uri = request.slice;
40
-
41
-
match jobs::enqueue_sync_job(&state.database_pool, user_did, slice_uri, request.params).await {
42
-
Ok(job_id) => Ok(Json(SyncJobResponse {
43
-
success: true,
44
-
job_id: Some(job_id),
45
-
message: format!("Sync job {} enqueued successfully", job_id),
46
-
})),
47
-
Err(e) => {
48
-
tracing::error!("Failed to enqueue sync job: {}", e);
49
-
Ok(Json(SyncJobResponse {
50
-
success: false,
51
-
job_id: None,
52
-
message: format!("Failed to enqueue sync job: {}", e),
53
-
}))
54
-
}
55
-
}
56
-
}
57
-
58
-
#[derive(Deserialize)]
59
-
#[serde(rename_all = "camelCase")]
60
-
pub struct SyncUserCollectionsRequest {
61
-
pub slice: String,
62
-
#[serde(default = "default_timeout")]
63
-
pub timeout_seconds: u64,
64
-
}
65
-
66
-
fn default_timeout() -> u64 {
67
-
30
68
-
}
69
-
70
-
pub async fn sync_user_collections(
71
-
State(state): State<AppState>,
72
-
headers: HeaderMap,
73
-
Json(request): Json<SyncUserCollectionsRequest>,
74
-
) -> Result<Json<crate::sync::SyncUserCollectionsResult>, (StatusCode, Json<serde_json::Value>)> {
75
-
let token = auth::extract_bearer_token(&headers).map_err(|e| {
76
-
(
77
-
StatusCode::UNAUTHORIZED,
78
-
Json(serde_json::json!({
79
-
"error": "AuthenticationRequired",
80
-
"message": format!("Bearer token required: {}", e)
81
-
})),
82
-
)
83
-
})?;
84
-
85
-
let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url)
86
-
.await
87
-
.map_err(|e| {
88
-
(
89
-
StatusCode::UNAUTHORIZED,
90
-
Json(serde_json::json!({
91
-
"error": "InvalidToken",
92
-
"message": format!("Token verification failed: {}", e)
93
-
})),
94
-
)
95
-
})?;
96
-
97
-
let user_did = user_info.did.unwrap_or(user_info.sub);
98
-
99
-
info!(
100
-
"🔄 Starting user collections sync for {} on slice {} (timeout: {}s)",
101
-
user_did, request.slice, request.timeout_seconds
102
-
);
103
-
104
-
if request.timeout_seconds > 300 {
105
-
return Err((
106
-
StatusCode::BAD_REQUEST,
107
-
Json(serde_json::json!({
108
-
"error": "InvalidTimeout",
109
-
"message": "Maximum timeout is 300 seconds (5 minutes)"
110
-
})),
111
-
));
112
-
}
113
-
114
-
let sync_service =
115
-
crate::sync::SyncService::new(state.database.clone(), state.config.relay_endpoint.clone());
116
-
117
-
match sync_service
118
-
.sync_user_collections(&user_did, &request.slice, request.timeout_seconds)
119
-
.await
120
-
{
121
-
Ok(result) => {
122
-
if result.timed_out {
123
-
info!(
124
-
"⏰ Sync timed out for user {}, suggesting async job",
125
-
user_did
126
-
);
127
-
} else {
128
-
info!(
129
-
"✅ Sync completed for user {}: {} repos, {} records",
130
-
user_did, result.repos_processed, result.records_synced
131
-
);
132
-
}
133
-
Ok(Json(result))
134
-
}
135
-
Err(e) => {
136
-
warn!("❌ Sync failed for user {}: {}", user_did, e);
137
-
Err((
138
-
StatusCode::INTERNAL_SERVER_ERROR,
139
-
Json(serde_json::json!({
140
-
"error": "SyncFailed",
141
-
"message": format!("Sync operation failed: {}", e)
142
-
})),
143
-
))
144
-
}
145
-
}
146
-
}
-73
api/src/api/upload_blob.rs
-73
api/src/api/upload_blob.rs
···
1
-
use axum::{
2
-
extract::{State, Request},
3
-
http::StatusCode,
4
-
response::Json,
5
-
};
6
-
use serde::{Deserialize, Serialize};
7
-
use crate::atproto_extensions::upload_blob as atproto_upload_blob;
8
-
use axum::body::to_bytes;
9
-
10
-
use crate::auth::{extract_bearer_token, verify_oauth_token, get_atproto_auth_for_user};
11
-
use crate::AppState;
12
-
13
-
// We need to use atproto-client's internal HTTP mechanism instead of manual DPoP
14
-
// Let me try a different approach - use the same HTTP client that create_record uses
15
-
16
-
#[derive(Serialize, Deserialize)]
17
-
pub struct BlobRef {
18
-
#[serde(rename = "$type")]
19
-
pub blob_type: String,
20
-
pub r#ref: String,
21
-
#[serde(rename = "mimeType")]
22
-
pub mime_type: String,
23
-
pub size: u64,
24
-
}
25
-
26
-
// Handler for com.atproto.repo.uploadBlob
27
-
pub async fn upload_blob(
28
-
State(state): State<AppState>,
29
-
request: Request,
30
-
) -> Result<Json<serde_json::Value>, StatusCode> {
31
-
32
-
// Extract headers from the request
33
-
let headers = request.headers().clone();
34
-
35
-
// Extract and verify OAuth token
36
-
let token = extract_bearer_token(&headers)?;
37
-
let _user_info = verify_oauth_token(&token, &state.config.auth_base_url).await?;
38
-
39
-
// Get AT Protocol DPoP auth and PDS URL
40
-
let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await?;
41
-
42
-
// Get mime type from Content-Type header
43
-
let mime_type = headers
44
-
.get("content-type")
45
-
.and_then(|v| v.to_str().ok())
46
-
.unwrap_or("application/octet-stream")
47
-
.to_string();
48
-
49
-
// Extract binary data from request body
50
-
let body = request.into_body();
51
-
let blob_data = to_bytes(body, usize::MAX)
52
-
.await
53
-
.map_err(|_| StatusCode::BAD_REQUEST)?
54
-
.to_vec();
55
-
56
-
// Create HTTP client (same as dynamic handlers)
57
-
let http_client = reqwest::Client::new();
58
-
59
-
// Use our atproto extension that follows the same pattern as create_record, put_record, etc.
60
-
let upload_result = atproto_upload_blob(
61
-
&http_client,
62
-
&dpop_auth,
63
-
&pds_url,
64
-
blob_data,
65
-
&mime_type
66
-
).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
67
-
68
-
// Convert to the expected JSON response format
69
-
let upload_response = serde_json::to_value(upload_result).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
70
-
71
-
// Return the blob reference
72
-
Ok(Json(upload_response))
73
-
}
-4
api/src/api/xrpc_dynamic.rs
-4
api/src/api/xrpc_dynamic.rs
···
242
242
.collect();
243
243
244
244
let output = SliceRecordsOutput {
245
-
success: true,
246
245
records: indexed_records,
247
246
cursor,
248
-
message: None,
249
247
};
250
248
251
249
Ok(Json(
···
369
367
.collect();
370
368
371
369
let output = SliceRecordsOutput {
372
-
success: true,
373
370
records: indexed_records,
374
371
cursor,
375
-
message: None,
376
372
};
377
373
378
374
Ok(Json(serde_json::to_value(output).map_err(|_| {
+1
-1
api/src/atproto_extensions.rs
+1
-1
api/src/atproto_extensions.rs
+2
-2
api/src/auth.rs
+2
-2
api/src/auth.rs
···
80
80
// Extract PDS URL from session
81
81
let pds_url = session_data["pds_endpoint"]
82
82
.as_str()
83
-
.ok_or_else(|| {
83
+
.ok_or({
84
84
StatusCode::INTERNAL_SERVER_ERROR
85
85
})?
86
86
.to_string();
···
89
89
// Extract AT Protocol access token from session data
90
90
let atproto_access_token = session_data["access_token"]
91
91
.as_str()
92
-
.ok_or_else(|| {
92
+
.ok_or({
93
93
StatusCode::INTERNAL_SERVER_ERROR
94
94
})?
95
95
.to_string();
+8
-10
api/src/database.rs
+8
-10
api/src/database.rs
···
150
150
) -> sqlx::query::QueryAs<'q, sqlx::Postgres, Record, sqlx::postgres::PgArguments> {
151
151
if let Some(clause) = where_clause {
152
152
// Bind AND condition parameters
153
-
for (_, condition) in &clause.conditions {
153
+
for condition in clause.conditions.values() {
154
154
query_builder = bind_single_condition(query_builder, condition);
155
155
}
156
156
157
157
// Bind OR condition parameters
158
158
if let Some(or_conditions) = &clause.or_conditions {
159
-
for (_, condition) in or_conditions {
159
+
for condition in or_conditions.values() {
160
160
query_builder = bind_single_condition(query_builder, condition);
161
161
}
162
162
}
···
379
379
.bind(&record.did)
380
380
.bind(&record.collection)
381
381
.bind(&record.json)
382
-
.bind(&record.indexed_at)
382
+
.bind(record.indexed_at)
383
383
.bind(&record.slice_uri);
384
384
}
385
385
···
885
885
.as_ref()
886
886
.and_then(|wc| wc.conditions.get("collection"))
887
887
.and_then(|c| c.eq.as_ref())
888
-
.and_then(|v| v.as_str())
889
-
.map_or(false, |s| s == "network.slices.lexicon");
888
+
.and_then(|v| v.as_str()) == Some("network.slices.lexicon");
890
889
891
890
if is_lexicon {
892
891
where_clauses.push(format!("json->>'slice' = ${}", param_count));
···
972
971
.as_ref()
973
972
.and_then(|wc| wc.conditions.get("collection"))
974
973
.and_then(|c| c.eq.as_ref())
975
-
.and_then(|v| v.as_str())
976
-
.map_or(false, |s| s == "network.slices.lexicon");
974
+
.and_then(|v| v.as_str()) == Some("network.slices.lexicon");
977
975
978
976
if is_lexicon {
979
977
where_clauses.push(format!("json->>'slice' = ${}", param_count));
···
1009
1007
// Bind where condition values using helper
1010
1008
if let Some(clause) = where_clause {
1011
1009
// Bind AND condition parameters
1012
-
for (_, condition) in &clause.conditions {
1010
+
for condition in clause.conditions.values() {
1013
1011
if let Some(eq_value) = &condition.eq {
1014
1012
if let Some(str_val) = eq_value.as_str() {
1015
1013
query_builder = query_builder.bind(str_val);
···
1031
1029
1032
1030
// Bind OR condition parameters
1033
1031
if let Some(or_conditions) = &clause.or_conditions {
1034
-
for (_, condition) in or_conditions {
1032
+
for condition in or_conditions.values() {
1035
1033
if let Some(eq_value) = &condition.eq {
1036
1034
if let Some(str_val) = eq_value.as_str() {
1037
1035
query_builder = query_builder.bind(str_val);
···
1096
1094
.bind(&record.did)
1097
1095
.bind(&record.collection)
1098
1096
.bind(&record.json)
1099
-
.bind(&record.indexed_at)
1097
+
.bind(record.indexed_at)
1100
1098
.bind(&record.slice_uri)
1101
1099
.fetch_one(&self.pool)
1102
1100
.await?;
+29
-8
api/src/errors.rs
+29
-8
api/src/errors.rs
···
58
58
59
59
#[error("error-slices-app-6 Bad request: {0}")]
60
60
BadRequest(String),
61
+
62
+
#[error("error-slices-app-7 Authentication required: {0}")]
63
+
AuthRequired(String),
64
+
65
+
#[error("error-slices-app-8 Forbidden: {0}")]
66
+
Forbidden(String),
67
+
}
68
+
69
+
impl From<StatusCode> for AppError {
70
+
fn from(status: StatusCode) -> Self {
71
+
match status {
72
+
StatusCode::BAD_REQUEST => AppError::BadRequest("Bad request".to_string()),
73
+
StatusCode::UNAUTHORIZED => AppError::AuthRequired("Authentication required".to_string()),
74
+
StatusCode::FORBIDDEN => AppError::Forbidden("Forbidden".to_string()),
75
+
StatusCode::NOT_FOUND => AppError::NotFound("Not found".to_string()),
76
+
_ => AppError::Internal(format!("HTTP error: {}", status)),
77
+
}
78
+
}
61
79
}
62
80
63
81
#[derive(Error, Debug)]
···
72
90
73
91
impl IntoResponse for AppError {
74
92
fn into_response(self) -> Response {
75
-
let (status, error_message) = match self {
76
-
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
77
-
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
78
-
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
79
-
AppError::DatabaseConnection(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
80
-
AppError::Migration(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
81
-
AppError::ServerBind(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
93
+
let (status, error_name, error_message) = match &self {
94
+
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "BadRequest", msg.clone()),
95
+
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, "NotFound", msg.clone()),
96
+
AppError::AuthRequired(msg) => (StatusCode::UNAUTHORIZED, "AuthenticationRequired", msg.clone()),
97
+
AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, "Forbidden", msg.clone()),
98
+
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", msg.clone()),
99
+
AppError::DatabaseConnection(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
100
+
AppError::Migration(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
101
+
AppError::ServerBind(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
82
102
};
83
103
84
104
let body = Json(serde_json::json!({
85
-
"error": error_message
105
+
"error": error_name,
106
+
"message": error_message
86
107
}));
87
108
88
109
(status, body).into_response()
+1
-1
api/src/jetstream.rs
+1
-1
api/src/jetstream.rs
+1
-1
api/src/jobs.rs
+1
-1
api/src/jobs.rs
···
205
205
);
206
206
207
207
let collections_json = serde_json::to_value(&result.collections_synced)
208
-
.map_err(|e| sqlx::Error::Protocol(format!("Failed to serialize collections: {}", e).into()))?;
208
+
.map_err(|e| sqlx::Error::Protocol(format!("Failed to serialize collections: {}", e)))?;
209
209
210
210
sqlx::query!(
211
211
r#"
+2
-2
api/src/logging.rs
+2
-2
api/src/logging.rs
···
233
233
for entry in batch.iter() {
234
234
sqlx_query = sqlx_query
235
235
.bind(&entry.log_type)
236
-
.bind(&entry.job_id)
236
+
.bind(entry.job_id)
237
237
.bind(&entry.user_did)
238
238
.bind(&entry.slice_uri)
239
239
.bind(&entry.level)
240
240
.bind(&entry.message)
241
241
.bind(&entry.metadata)
242
-
.bind(&entry.created_at);
242
+
.bind(entry.created_at);
243
243
}
244
244
245
245
// Execute batch insert
+18
-18
api/src/main.rs
+18
-18
api/src/main.rs
···
9
9
mod logging;
10
10
mod models;
11
11
mod sync;
12
+
mod xrpc;
12
13
13
14
use axum::{
14
15
Router,
···
20
21
use std::sync::atomic::AtomicBool;
21
22
use tower_http::{cors::CorsLayer, trace::TraceLayer};
22
23
use tracing::info;
23
-
use tracing_subscriber;
24
24
25
25
use crate::database::Database;
26
26
use crate::errors::AppError;
···
277
277
// XRPC endpoints
278
278
.route(
279
279
"/xrpc/com.atproto.repo.uploadBlob",
280
-
post(api::upload_blob::upload_blob),
280
+
post(xrpc::com::atproto::repo::upload_blob::handler),
281
281
)
282
282
.route(
283
283
"/xrpc/network.slices.slice.startSync",
284
-
post(api::sync::sync),
284
+
post(xrpc::network::slices::slice::start_sync::handler),
285
285
)
286
286
.route(
287
287
"/xrpc/network.slices.slice.syncUserCollections",
288
-
post(api::sync::sync_user_collections),
288
+
post(xrpc::network::slices::slice::sync_user_collections::handler),
289
289
)
290
290
.route(
291
291
"/xrpc/network.slices.slice.getJobStatus",
292
-
get(api::jobs::get_job_status),
292
+
get(xrpc::network::slices::slice::get_job_status::handler),
293
293
)
294
294
.route(
295
295
"/xrpc/network.slices.slice.getJobHistory",
296
-
get(api::jobs::get_slice_job_history),
296
+
get(xrpc::network::slices::slice::get_job_history::handler),
297
297
)
298
298
.route(
299
299
"/xrpc/network.slices.slice.getJobLogs",
300
-
get(api::logs::get_sync_job_logs_handler),
300
+
get(xrpc::network::slices::slice::get_job_logs::handler),
301
301
)
302
302
.route(
303
303
"/xrpc/network.slices.slice.getJetstreamLogs",
304
-
get(api::logs::get_jetstream_logs_handler),
304
+
get(xrpc::network::slices::slice::get_jetstream_logs::handler),
305
305
)
306
306
.route(
307
307
"/xrpc/network.slices.slice.stats",
308
-
post(api::stats::stats),
308
+
get(xrpc::network::slices::slice::stats::handler),
309
309
)
310
310
.route(
311
311
"/xrpc/network.slices.slice.getSparklines",
312
-
post(api::sparkline::batch_sparkline),
312
+
post(xrpc::network::slices::slice::get_sparklines::handler),
313
313
)
314
314
.route(
315
315
"/xrpc/network.slices.slice.getSliceRecords",
316
-
post(api::records::get_records),
316
+
post(xrpc::network::slices::slice::get_slice_records::handler),
317
317
)
318
318
.route(
319
319
"/xrpc/network.slices.slice.openapi",
320
-
get(api::openapi::get_openapi_spec),
320
+
get(xrpc::network::slices::slice::openapi::handler),
321
321
)
322
322
.route(
323
323
"/xrpc/network.slices.slice.getJetstreamStatus",
324
-
get(api::jetstream::get_jetstream_status),
324
+
get(xrpc::network::slices::slice::get_jetstream_status::handler),
325
325
)
326
326
.route(
327
327
"/xrpc/network.slices.slice.getActors",
328
-
post(api::actors::get_actors),
328
+
post(xrpc::network::slices::slice::get_actors::handler),
329
329
)
330
330
.route(
331
331
"/xrpc/network.slices.slice.createOAuthClient",
332
-
post(api::oauth::create_oauth_client),
332
+
post(xrpc::network::slices::slice::create_oauth_client::handler),
333
333
)
334
334
.route(
335
335
"/xrpc/network.slices.slice.getOAuthClients",
336
-
get(api::oauth::get_oauth_clients),
336
+
get(xrpc::network::slices::slice::get_oauth_clients::handler),
337
337
)
338
338
.route(
339
339
"/xrpc/network.slices.slice.updateOAuthClient",
340
-
post(api::oauth::update_oauth_client),
340
+
post(xrpc::network::slices::slice::update_oauth_client::handler),
341
341
)
342
342
.route(
343
343
"/xrpc/network.slices.slice.deleteOAuthClient",
344
-
post(api::oauth::delete_oauth_client),
344
+
post(xrpc::network::slices::slice::delete_oauth_client::handler),
345
345
)
346
346
// Dynamic collection-specific XRPC endpoints (wildcard routes must come last)
347
347
.route(
-111
api/src/models.rs
-111
api/src/models.rs
···
26
26
pub indexed_at: String,
27
27
}
28
28
29
-
#[derive(Debug, Serialize, Deserialize)]
30
-
#[serde(rename_all = "camelCase")]
31
-
pub struct ListRecordsOutput {
32
-
pub records: Vec<IndexedRecord>,
33
-
pub cursor: Option<String>,
34
-
}
35
-
36
29
#[derive(Debug, Clone, Serialize, Deserialize)]
37
30
#[serde(rename_all = "camelCase")]
38
31
pub struct BulkSyncParams {
···
43
36
pub skip_validation: Option<bool>,
44
37
}
45
38
46
-
#[derive(Debug, Serialize, Deserialize)]
47
-
#[serde(rename_all = "camelCase")]
48
-
pub struct BulkSyncOutput {
49
-
pub success: bool,
50
-
pub total_records: i64,
51
-
pub collections_synced: Vec<String>,
52
-
pub repos_processed: i64,
53
-
pub message: String,
54
-
}
55
-
56
39
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
57
40
#[serde(rename_all = "camelCase")]
58
41
pub struct Actor {
···
72
55
73
56
#[derive(Debug, Serialize, Deserialize)]
74
57
#[serde(rename_all = "camelCase")]
75
-
pub struct SliceStatsParams {
76
-
pub slice: String,
77
-
}
78
-
79
-
#[derive(Debug, Serialize, Deserialize)]
80
-
#[serde(rename_all = "camelCase")]
81
-
pub struct SliceStatsOutput {
82
-
pub success: bool,
83
-
pub collections: Vec<String>,
84
-
pub collection_stats: Vec<CollectionStats>,
85
-
pub total_lexicons: i64,
86
-
pub total_records: i64,
87
-
pub total_actors: i64,
88
-
pub message: Option<String>,
89
-
}
90
-
91
-
#[derive(Debug, Serialize, Deserialize)]
92
-
#[serde(rename_all = "camelCase")]
93
58
pub struct WhereCondition {
94
59
pub eq: Option<Value>,
95
60
#[serde(rename = "in")]
···
128
93
#[derive(Debug, Serialize, Deserialize)]
129
94
#[serde(rename_all = "camelCase")]
130
95
pub struct SliceRecordsOutput {
131
-
pub success: bool,
132
96
pub records: Vec<IndexedRecord>,
133
97
pub cursor: Option<String>,
134
-
pub message: Option<String>,
135
98
}
136
99
137
100
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
···
147
110
148
111
#[derive(Debug, Serialize, Deserialize)]
149
112
#[serde(rename_all = "camelCase")]
150
-
pub struct CreateOAuthClientRequest {
151
-
pub slice_uri: String,
152
-
pub client_name: String,
153
-
pub redirect_uris: Vec<String>,
154
-
pub grant_types: Option<Vec<String>>,
155
-
pub response_types: Option<Vec<String>>,
156
-
pub scope: Option<String>,
157
-
pub client_uri: Option<String>,
158
-
pub logo_uri: Option<String>,
159
-
pub tos_uri: Option<String>,
160
-
pub policy_uri: Option<String>,
161
-
}
162
-
163
-
#[derive(Debug, Serialize, Deserialize)]
164
-
#[serde(rename_all = "camelCase")]
165
-
pub struct OAuthClientDetails {
166
-
pub client_id: String,
167
-
pub client_secret: Option<String>,
168
-
pub client_name: String,
169
-
pub redirect_uris: Vec<String>,
170
-
pub grant_types: Vec<String>,
171
-
pub response_types: Vec<String>,
172
-
pub scope: Option<String>,
173
-
pub client_uri: Option<String>,
174
-
pub logo_uri: Option<String>,
175
-
pub tos_uri: Option<String>,
176
-
pub policy_uri: Option<String>,
177
-
pub created_at: DateTime<Utc>,
178
-
pub created_by_did: String,
179
-
}
180
-
181
-
#[derive(Debug, Serialize, Deserialize)]
182
-
#[serde(rename_all = "camelCase")]
183
-
pub struct ListOAuthClientsResponse {
184
-
pub clients: Vec<OAuthClientDetails>,
185
-
}
186
-
187
-
#[derive(Debug, Serialize, Deserialize)]
188
-
#[serde(rename_all = "camelCase")]
189
-
pub struct UpdateOAuthClientRequest {
190
-
pub client_id: String,
191
-
pub client_name: Option<String>,
192
-
pub redirect_uris: Option<Vec<String>>,
193
-
pub scope: Option<String>,
194
-
pub client_uri: Option<String>,
195
-
pub logo_uri: Option<String>,
196
-
pub tos_uri: Option<String>,
197
-
pub policy_uri: Option<String>,
198
-
}
199
-
200
-
#[derive(Debug, Serialize, Deserialize)]
201
-
#[serde(rename_all = "camelCase")]
202
-
pub struct DeleteOAuthClientResponse {
203
-
pub success: bool,
204
-
pub message: String,
205
-
}
206
-
207
-
208
-
#[derive(Debug, Serialize, Deserialize)]
209
-
#[serde(rename_all = "camelCase")]
210
113
pub struct SparklinePoint {
211
114
pub timestamp: String, // ISO 8601
212
115
pub count: i64,
213
116
}
214
117
215
-
#[derive(Debug, Serialize, Deserialize)]
216
-
#[serde(rename_all = "camelCase")]
217
-
pub struct GetSparklinesParams {
218
-
pub slices: Vec<String>,
219
-
pub interval: Option<String>, // "hour", "day", "minute" - defaults to "hour"
220
-
pub duration: Option<String>, // "24h", "7d", "30d" - defaults to "24h"
221
-
}
222
118
223
-
#[derive(Debug, Serialize, Deserialize)]
224
-
#[serde(rename_all = "camelCase")]
225
-
pub struct GetSparklinesOutput {
226
-
pub success: bool,
227
-
pub sparklines: std::collections::HashMap<String, Vec<SparklinePoint>>,
228
-
pub message: Option<String>,
229
-
}
+3
-4
api/src/sync.rs
+3
-4
api/src/sync.rs
···
192
192
for collection in &all_collections {
193
193
requests_by_pds
194
194
.entry(pds_url.clone())
195
-
.or_insert_with(Vec::new)
195
+
.or_default()
196
196
.push((repo.clone(), collection.clone()));
197
197
}
198
198
}
···
535
535
536
536
for atproto_record in list_response.records {
537
537
// Check if we already have this record with the same CID
538
-
if let Some(existing_cid) = existing_cids.get(&atproto_record.uri) {
539
-
if existing_cid == &atproto_record.cid {
538
+
if let Some(existing_cid) = existing_cids.get(&atproto_record.uri)
539
+
&& existing_cid == &atproto_record.cid {
540
540
// Record unchanged, skip it
541
541
skipped_count += 1;
542
542
continue;
543
543
}
544
-
}
545
544
546
545
// Record is new or changed, include it
547
546
let record = Record {
+1
api/src/xrpc/com/atproto/mod.rs
+1
api/src/xrpc/com/atproto/mod.rs
···
1
+
pub mod repo;
+1
api/src/xrpc/com/atproto/repo/mod.rs
+1
api/src/xrpc/com/atproto/repo/mod.rs
···
1
+
pub mod upload_blob;
+39
api/src/xrpc/com/atproto/repo/upload_blob.rs
+39
api/src/xrpc/com/atproto/repo/upload_blob.rs
···
1
+
use axum::{extract::{Request, State}, response::Json};
2
+
use axum::body::to_bytes;
3
+
use crate::{AppState, auth, atproto_extensions::upload_blob as atproto_upload_blob, errors::AppError};
4
+
5
+
pub async fn handler(
6
+
State(state): State<AppState>,
7
+
request: Request,
8
+
) -> Result<Json<serde_json::Value>, AppError> {
9
+
let headers = request.headers().clone();
10
+
11
+
let token = auth::extract_bearer_token(&headers)?;
12
+
let _user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
13
+
14
+
let (dpop_auth, pds_url) =
15
+
auth::get_atproto_auth_for_user(&token, &state.config.auth_base_url).await?;
16
+
17
+
let mime_type = headers
18
+
.get("content-type")
19
+
.and_then(|v| v.to_str().ok())
20
+
.unwrap_or("application/octet-stream")
21
+
.to_string();
22
+
23
+
let body = request.into_body();
24
+
let blob_data = to_bytes(body, usize::MAX)
25
+
.await
26
+
.map_err(|_| AppError::BadRequest("Invalid request body".to_string()))?
27
+
.to_vec();
28
+
29
+
let http_client = reqwest::Client::new();
30
+
31
+
let upload_result = atproto_upload_blob(&http_client, &dpop_auth, &pds_url, blob_data, &mime_type)
32
+
.await
33
+
.map_err(|e| AppError::Internal(format!("Failed to upload blob: {}", e)))?;
34
+
35
+
let upload_response = serde_json::to_value(upload_result)
36
+
.map_err(|e| AppError::Internal(format!("Failed to serialize response: {}", e)))?;
37
+
38
+
Ok(Json(upload_response))
39
+
}
+1
api/src/xrpc/com/mod.rs
+1
api/src/xrpc/com/mod.rs
···
1
+
pub mod atproto;
+1
api/src/xrpc/network/mod.rs
+1
api/src/xrpc/network/mod.rs
···
1
+
pub mod slices;
+1
api/src/xrpc/network/slices/mod.rs
+1
api/src/xrpc/network/slices/mod.rs
···
1
+
pub mod slice;
+176
api/src/xrpc/network/slices/slice/create_oauth_client.rs
+176
api/src/xrpc/network/slices/slice/create_oauth_client.rs
···
1
+
use axum::{extract::State, http::HeaderMap, response::Json};
2
+
use reqwest::Client;
3
+
use serde::{Deserialize, Serialize};
4
+
use crate::{AppState, auth, errors::AppError};
5
+
6
+
#[derive(Debug, Deserialize)]
7
+
#[serde(rename_all = "camelCase")]
8
+
pub struct Params {
9
+
pub slice_uri: String,
10
+
pub client_name: String,
11
+
pub redirect_uris: Vec<String>,
12
+
pub grant_types: Option<Vec<String>>,
13
+
pub response_types: Option<Vec<String>>,
14
+
pub scope: Option<String>,
15
+
pub client_uri: Option<String>,
16
+
pub logo_uri: Option<String>,
17
+
pub tos_uri: Option<String>,
18
+
pub policy_uri: Option<String>,
19
+
}
20
+
21
+
#[derive(Debug, Serialize)]
22
+
#[serde(rename_all = "camelCase")]
23
+
pub struct Output {
24
+
pub client_id: String,
25
+
pub client_secret: Option<String>,
26
+
pub client_name: String,
27
+
pub redirect_uris: Vec<String>,
28
+
pub grant_types: Vec<String>,
29
+
pub response_types: Vec<String>,
30
+
pub scope: Option<String>,
31
+
pub client_uri: Option<String>,
32
+
pub logo_uri: Option<String>,
33
+
pub tos_uri: Option<String>,
34
+
pub policy_uri: Option<String>,
35
+
pub created_at: chrono::DateTime<chrono::Utc>,
36
+
pub created_by_did: String,
37
+
}
38
+
39
+
#[derive(Debug, Serialize, Deserialize)]
40
+
#[serde(rename_all = "snake_case")]
41
+
struct AipClientRequest {
42
+
pub client_name: String,
43
+
pub redirect_uris: Vec<String>,
44
+
#[serde(skip_serializing_if = "Option::is_none")]
45
+
pub grant_types: Option<Vec<String>>,
46
+
#[serde(skip_serializing_if = "Option::is_none")]
47
+
pub response_types: Option<Vec<String>>,
48
+
#[serde(skip_serializing_if = "Option::is_none")]
49
+
pub scope: Option<String>,
50
+
#[serde(skip_serializing_if = "Option::is_none")]
51
+
pub client_uri: Option<String>,
52
+
#[serde(skip_serializing_if = "Option::is_none")]
53
+
pub logo_uri: Option<String>,
54
+
#[serde(skip_serializing_if = "Option::is_none")]
55
+
pub tos_uri: Option<String>,
56
+
#[serde(skip_serializing_if = "Option::is_none")]
57
+
pub policy_uri: Option<String>,
58
+
}
59
+
60
+
#[derive(Serialize, Deserialize)]
61
+
#[serde(rename_all = "snake_case")]
62
+
struct AipClientResponse {
63
+
pub client_id: String,
64
+
pub client_secret: Option<String>,
65
+
pub registration_access_token: Option<String>,
66
+
pub client_name: String,
67
+
pub redirect_uris: Vec<String>,
68
+
pub grant_types: Vec<String>,
69
+
pub response_types: Vec<String>,
70
+
pub scope: Option<String>,
71
+
pub client_uri: Option<String>,
72
+
pub logo_uri: Option<String>,
73
+
pub tos_uri: Option<String>,
74
+
pub policy_uri: Option<String>,
75
+
}
76
+
77
+
pub async fn handler(
78
+
State(state): State<AppState>,
79
+
headers: HeaderMap,
80
+
Json(params): Json<Params>,
81
+
) -> Result<Json<Output>, AppError> {
82
+
if params.client_name.trim().is_empty() {
83
+
return Err(AppError::BadRequest("Client name cannot be empty".to_string()));
84
+
}
85
+
86
+
if params.redirect_uris.is_empty() {
87
+
return Err(AppError::BadRequest(
88
+
"At least one redirect URI is required".to_string(),
89
+
));
90
+
}
91
+
92
+
for uri in ¶ms.redirect_uris {
93
+
if !uri.starts_with("http://") && !uri.starts_with("https://") {
94
+
return Err(AppError::BadRequest(format!(
95
+
"Redirect URI must use HTTP or HTTPS: {}",
96
+
uri
97
+
)));
98
+
}
99
+
if uri.trim().is_empty() {
100
+
return Err(AppError::BadRequest("Redirect URI cannot be empty".to_string()));
101
+
}
102
+
}
103
+
104
+
let token = auth::extract_bearer_token(&headers)?;
105
+
let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
106
+
107
+
let user_did = user_info.sub;
108
+
109
+
let client = Client::new();
110
+
let aip_request = AipClientRequest {
111
+
client_name: params.client_name.clone(),
112
+
redirect_uris: params.redirect_uris.clone(),
113
+
grant_types: params.grant_types.clone(),
114
+
response_types: params.response_types.clone(),
115
+
scope: params.scope.clone(),
116
+
client_uri: params.client_uri.clone(),
117
+
logo_uri: params.logo_uri.clone(),
118
+
tos_uri: params.tos_uri.clone(),
119
+
policy_uri: params.policy_uri.clone(),
120
+
};
121
+
122
+
let registration_url = format!("{}/oauth/clients/register", state.config.auth_base_url);
123
+
124
+
let aip_response = client
125
+
.post(®istration_url)
126
+
.json(&aip_request)
127
+
.send()
128
+
.await
129
+
.map_err(|e| AppError::Internal(format!("Failed to register client with AIP: {}", e)))?;
130
+
131
+
if !aip_response.status().is_success() {
132
+
let error_text = aip_response
133
+
.text()
134
+
.await
135
+
.unwrap_or_else(|_| "Unknown error".to_string());
136
+
return Err(AppError::Internal(format!(
137
+
"AIP registration failed: {}",
138
+
error_text
139
+
)));
140
+
}
141
+
142
+
let response_body = aip_response
143
+
.text()
144
+
.await
145
+
.map_err(|e| AppError::Internal(format!("Failed to get response body: {}", e)))?;
146
+
147
+
let aip_client: AipClientResponse = serde_json::from_str(&response_body)
148
+
.map_err(|e| AppError::Internal(format!("Failed to parse AIP response: {}", e)))?;
149
+
150
+
let oauth_client = state
151
+
.database
152
+
.create_oauth_client(
153
+
¶ms.slice_uri,
154
+
&aip_client.client_id,
155
+
aip_client.registration_access_token.as_deref(),
156
+
&user_did,
157
+
)
158
+
.await
159
+
.map_err(|e| AppError::Internal(format!("Failed to store OAuth client: {}", e)))?;
160
+
161
+
Ok(Json(Output {
162
+
client_id: aip_client.client_id,
163
+
client_secret: aip_client.client_secret,
164
+
client_name: aip_client.client_name,
165
+
redirect_uris: aip_client.redirect_uris,
166
+
grant_types: aip_client.grant_types,
167
+
response_types: aip_client.response_types,
168
+
scope: aip_client.scope,
169
+
client_uri: aip_client.client_uri,
170
+
logo_uri: aip_client.logo_uri,
171
+
tos_uri: aip_client.tos_uri,
172
+
policy_uri: aip_client.policy_uri,
173
+
created_at: oauth_client.created_at,
174
+
created_by_did: oauth_client.created_by_did,
175
+
}))
176
+
}
+54
api/src/xrpc/network/slices/slice/delete_oauth_client.rs
+54
api/src/xrpc/network/slices/slice/delete_oauth_client.rs
···
1
+
use axum::{extract::State, http::HeaderMap, response::Json};
2
+
use reqwest::Client;
3
+
use serde::{Deserialize, Serialize};
4
+
use crate::{AppState, auth, errors::AppError};
5
+
6
+
#[derive(Debug, Deserialize)]
7
+
#[serde(rename_all = "camelCase")]
8
+
pub struct Params {
9
+
pub client_id: String,
10
+
}
11
+
12
+
#[derive(Debug, Serialize)]
13
+
#[serde(rename_all = "camelCase")]
14
+
pub struct Output {
15
+
pub message: String,
16
+
}
17
+
18
+
pub async fn handler(
19
+
State(state): State<AppState>,
20
+
headers: HeaderMap,
21
+
Json(params): Json<Params>,
22
+
) -> Result<Json<Output>, AppError> {
23
+
let token = auth::extract_bearer_token(&headers)?;
24
+
auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
25
+
26
+
let oauth_client = state
27
+
.database
28
+
.get_oauth_client_by_id(¶ms.client_id)
29
+
.await
30
+
.map_err(|e| AppError::Internal(format!("Failed to fetch OAuth client: {}", e)))?
31
+
.ok_or_else(|| AppError::NotFound("OAuth client not found".to_string()))?;
32
+
33
+
if let Some(registration_token) = &oauth_client.registration_access_token {
34
+
let client = Client::new();
35
+
let _aip_response = client
36
+
.delete(format!(
37
+
"{}/oauth/clients/{}",
38
+
state.config.auth_base_url, params.client_id
39
+
))
40
+
.bearer_auth(registration_token)
41
+
.send()
42
+
.await;
43
+
}
44
+
45
+
state
46
+
.database
47
+
.delete_oauth_client(¶ms.client_id)
48
+
.await
49
+
.map_err(|e| AppError::Internal(format!("Failed to delete OAuth client: {}", e)))?;
50
+
51
+
Ok(Json(Output {
52
+
message: format!("OAuth client {} deleted successfully", params.client_id),
53
+
}))
54
+
}
+39
api/src/xrpc/network/slices/slice/get_actors.rs
+39
api/src/xrpc/network/slices/slice/get_actors.rs
···
1
+
use axum::{extract::State, response::Json};
2
+
use serde::{Deserialize, Serialize};
3
+
use std::collections::HashMap;
4
+
use crate::{AppState, errors::AppError, models::{Actor, WhereCondition}};
5
+
6
+
#[derive(Debug, Deserialize)]
7
+
#[serde(rename_all = "camelCase")]
8
+
pub struct Params {
9
+
pub slice: String,
10
+
pub limit: Option<i32>,
11
+
pub cursor: Option<String>,
12
+
#[serde(rename = "where")]
13
+
pub where_conditions: Option<HashMap<String, WhereCondition>>,
14
+
}
15
+
16
+
#[derive(Debug, Serialize)]
17
+
#[serde(rename_all = "camelCase")]
18
+
pub struct Output {
19
+
pub actors: Vec<Actor>,
20
+
pub cursor: Option<String>,
21
+
}
22
+
23
+
pub async fn handler(
24
+
State(state): State<AppState>,
25
+
Json(params): Json<Params>,
26
+
) -> Result<Json<Output>, AppError> {
27
+
let (actors, cursor) = state
28
+
.database
29
+
.get_slice_actors(
30
+
¶ms.slice,
31
+
params.limit,
32
+
params.cursor.as_deref(),
33
+
params.where_conditions.as_ref(),
34
+
)
35
+
.await
36
+
.map_err(|e| AppError::Internal(format!("Failed to fetch actors: {}", e)))?;
37
+
38
+
Ok(Json(Output { actors, cursor }))
39
+
}
+25
api/src/xrpc/network/slices/slice/get_jetstream_logs.rs
+25
api/src/xrpc/network/slices/slice/get_jetstream_logs.rs
···
1
+
use axum::{extract::{Query, State}, response::Json};
2
+
use serde::{Deserialize, Serialize};
3
+
use crate::{AppState, errors::AppError, logging::{get_jetstream_logs, LogEntry}};
4
+
5
+
#[derive(Debug, Deserialize)]
6
+
pub struct Params {
7
+
pub limit: Option<i64>,
8
+
pub slice: Option<String>,
9
+
}
10
+
11
+
#[derive(Debug, Serialize)]
12
+
pub struct Output {
13
+
pub logs: Vec<LogEntry>,
14
+
}
15
+
16
+
pub async fn handler(
17
+
State(state): State<AppState>,
18
+
Query(params): Query<Params>,
19
+
) -> Result<Json<Output>, AppError> {
20
+
let logs = get_jetstream_logs(&state.database_pool, params.slice.as_deref(), params.limit)
21
+
.await
22
+
.map_err(|e| AppError::Internal(format!("Failed to get jetstream logs: {}", e)))?;
23
+
24
+
Ok(Json(Output { logs }))
25
+
}
+17
api/src/xrpc/network/slices/slice/get_jetstream_status.rs
+17
api/src/xrpc/network/slices/slice/get_jetstream_status.rs
···
1
+
use axum::{extract::State, response::Json};
2
+
use serde::Serialize;
3
+
use crate::AppState;
4
+
5
+
#[derive(Serialize)]
6
+
#[serde(rename_all = "camelCase")]
7
+
pub struct Output {
8
+
pub connected: bool,
9
+
}
10
+
11
+
pub async fn handler(State(state): State<AppState>) -> Json<Output> {
12
+
let connected = state
13
+
.jetstream_connected
14
+
.load(std::sync::atomic::Ordering::Relaxed);
15
+
16
+
Json(Output { connected })
17
+
}
+26
api/src/xrpc/network/slices/slice/get_job_history.rs
+26
api/src/xrpc/network/slices/slice/get_job_history.rs
···
1
+
use axum::{extract::{Query, State}, response::Json};
2
+
use serde::Deserialize;
3
+
use crate::{AppState, errors::AppError, jobs};
4
+
5
+
#[derive(Debug, Deserialize)]
6
+
#[serde(rename_all = "camelCase")]
7
+
pub struct Params {
8
+
pub user_did: String,
9
+
pub slice_uri: String,
10
+
pub limit: Option<i64>,
11
+
}
12
+
13
+
pub async fn handler(
14
+
State(state): State<AppState>,
15
+
Query(params): Query<Params>,
16
+
) -> Result<Json<Vec<jobs::JobStatus>>, AppError> {
17
+
jobs::get_slice_job_history(
18
+
&state.database_pool,
19
+
¶ms.user_did,
20
+
¶ms.slice_uri,
21
+
params.limit,
22
+
)
23
+
.await
24
+
.map(Json)
25
+
.map_err(|e| AppError::Internal(format!("Failed to get slice job history: {}", e)))
26
+
}
+27
api/src/xrpc/network/slices/slice/get_job_logs.rs
+27
api/src/xrpc/network/slices/slice/get_job_logs.rs
···
1
+
use axum::{extract::{Query, State}, response::Json};
2
+
use serde::{Deserialize, Serialize};
3
+
use uuid::Uuid;
4
+
use crate::{AppState, errors::AppError, logging::{get_sync_job_logs, LogEntry}};
5
+
6
+
#[derive(Debug, Deserialize)]
7
+
#[serde(rename_all = "camelCase")]
8
+
pub struct Params {
9
+
pub job_id: Uuid,
10
+
pub limit: Option<i64>,
11
+
}
12
+
13
+
#[derive(Debug, Serialize)]
14
+
pub struct Output {
15
+
pub logs: Vec<LogEntry>,
16
+
}
17
+
18
+
pub async fn handler(
19
+
State(state): State<AppState>,
20
+
Query(params): Query<Params>,
21
+
) -> Result<Json<Output>, AppError> {
22
+
let logs = get_sync_job_logs(&state.database_pool, params.job_id, params.limit)
23
+
.await
24
+
.map_err(|e| AppError::Internal(format!("Failed to get sync job logs: {}", e)))?;
25
+
26
+
Ok(Json(Output { logs }))
27
+
}
+21
api/src/xrpc/network/slices/slice/get_job_status.rs
+21
api/src/xrpc/network/slices/slice/get_job_status.rs
···
1
+
use axum::{extract::{Query, State}, response::Json};
2
+
use serde::Deserialize;
3
+
use uuid::Uuid;
4
+
use crate::{AppState, errors::AppError, jobs};
5
+
6
+
#[derive(Debug, Deserialize)]
7
+
#[serde(rename_all = "camelCase")]
8
+
pub struct Params {
9
+
pub job_id: Uuid,
10
+
}
11
+
12
+
pub async fn handler(
13
+
State(state): State<AppState>,
14
+
Query(params): Query<Params>,
15
+
) -> Result<Json<jobs::JobStatus>, AppError> {
16
+
match jobs::get_job_status(&state.database_pool, params.job_id).await {
17
+
Ok(Some(status)) => Ok(Json(status)),
18
+
Ok(None) => Err(AppError::NotFound(format!("Job {} not found", params.job_id))),
19
+
Err(e) => Err(AppError::Internal(format!("Failed to get job status: {}", e))),
20
+
}
21
+
}
+126
api/src/xrpc/network/slices/slice/get_oauth_clients.rs
+126
api/src/xrpc/network/slices/slice/get_oauth_clients.rs
···
1
+
use axum::{extract::{Query, State}, http::HeaderMap, response::Json};
2
+
use reqwest::Client;
3
+
use serde::{Deserialize, Serialize};
4
+
use crate::{AppState, auth, errors::AppError};
5
+
6
+
#[derive(Debug, Deserialize)]
7
+
#[serde(rename_all = "camelCase")]
8
+
pub struct Params {
9
+
pub slice: String,
10
+
}
11
+
12
+
#[derive(Debug, Serialize)]
13
+
#[serde(rename_all = "camelCase")]
14
+
pub struct OAuthClientDetails {
15
+
pub client_id: String,
16
+
pub client_secret: Option<String>,
17
+
pub client_name: String,
18
+
pub redirect_uris: Vec<String>,
19
+
pub grant_types: Vec<String>,
20
+
pub response_types: Vec<String>,
21
+
pub scope: Option<String>,
22
+
pub client_uri: Option<String>,
23
+
pub logo_uri: Option<String>,
24
+
pub tos_uri: Option<String>,
25
+
pub policy_uri: Option<String>,
26
+
pub created_at: chrono::DateTime<chrono::Utc>,
27
+
pub created_by_did: String,
28
+
}
29
+
30
+
#[derive(Debug, Serialize)]
31
+
#[serde(rename_all = "camelCase")]
32
+
pub struct Output {
33
+
pub clients: Vec<OAuthClientDetails>,
34
+
}
35
+
36
+
#[derive(Serialize, Deserialize)]
37
+
#[serde(rename_all = "snake_case")]
38
+
struct AipClientResponse {
39
+
pub client_id: String,
40
+
pub client_secret: Option<String>,
41
+
pub client_name: String,
42
+
pub redirect_uris: Vec<String>,
43
+
pub grant_types: Vec<String>,
44
+
pub response_types: Vec<String>,
45
+
pub scope: Option<String>,
46
+
pub client_uri: Option<String>,
47
+
pub logo_uri: Option<String>,
48
+
pub tos_uri: Option<String>,
49
+
pub policy_uri: Option<String>,
50
+
}
51
+
52
+
pub async fn handler(
53
+
State(state): State<AppState>,
54
+
headers: HeaderMap,
55
+
Query(params): Query<Params>,
56
+
) -> Result<Json<Output>, AppError> {
57
+
let token = auth::extract_bearer_token(&headers)?;
58
+
auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
59
+
60
+
let clients = state
61
+
.database
62
+
.get_oauth_clients_for_slice(¶ms.slice)
63
+
.await
64
+
.map_err(|e| AppError::Internal(format!("Failed to fetch OAuth clients: {}", e)))?;
65
+
66
+
if clients.is_empty() {
67
+
return Ok(Json(Output { clients: vec![] }));
68
+
}
69
+
70
+
let aip_base_url = &state.config.auth_base_url;
71
+
let client = Client::new();
72
+
let mut client_details = Vec::new();
73
+
74
+
for oauth_client in clients {
75
+
let aip_url = format!("{}/oauth/clients/{}", aip_base_url, oauth_client.client_id);
76
+
77
+
let mut request_builder = client.get(&aip_url);
78
+
if let Some(token) = &oauth_client.registration_access_token {
79
+
request_builder = request_builder.bearer_auth(token);
80
+
}
81
+
82
+
match request_builder.send().await {
83
+
Ok(response) if response.status().is_success() => {
84
+
if let Ok(response_text) = response.text().await
85
+
&& let Ok(aip_client) = serde_json::from_str::<AipClientResponse>(&response_text) {
86
+
client_details.push(OAuthClientDetails {
87
+
client_id: aip_client.client_id,
88
+
client_secret: aip_client.client_secret,
89
+
client_name: aip_client.client_name,
90
+
redirect_uris: aip_client.redirect_uris,
91
+
grant_types: aip_client.grant_types,
92
+
response_types: aip_client.response_types,
93
+
scope: aip_client.scope,
94
+
client_uri: aip_client.client_uri,
95
+
logo_uri: aip_client.logo_uri,
96
+
tos_uri: aip_client.tos_uri,
97
+
policy_uri: aip_client.policy_uri,
98
+
created_at: oauth_client.created_at,
99
+
created_by_did: oauth_client.created_by_did,
100
+
});
101
+
}
102
+
}
103
+
_ => {
104
+
client_details.push(OAuthClientDetails {
105
+
client_id: oauth_client.client_id,
106
+
client_secret: None,
107
+
client_name: "Unknown".to_string(),
108
+
redirect_uris: vec![],
109
+
grant_types: vec!["authorization_code".to_string()],
110
+
response_types: vec!["code".to_string()],
111
+
scope: None,
112
+
client_uri: None,
113
+
logo_uri: None,
114
+
tos_uri: None,
115
+
policy_uri: None,
116
+
created_at: oauth_client.created_at,
117
+
created_by_did: oauth_client.created_by_did,
118
+
});
119
+
}
120
+
}
121
+
}
122
+
123
+
Ok(Json(Output {
124
+
clients: client_details,
125
+
}))
126
+
}
+67
api/src/xrpc/network/slices/slice/get_slice_records.rs
+67
api/src/xrpc/network/slices/slice/get_slice_records.rs
···
1
+
use axum::{extract::State, response::Json};
2
+
use serde::{Deserialize, Serialize};
3
+
use serde_json::Value;
4
+
use crate::{AppState, errors::AppError, models::{WhereClause, SortField}};
5
+
6
+
#[derive(Debug, Deserialize)]
7
+
#[serde(rename_all = "camelCase")]
8
+
pub struct Params {
9
+
pub slice: String,
10
+
pub limit: Option<i32>,
11
+
pub cursor: Option<String>,
12
+
#[serde(rename = "where")]
13
+
pub where_clause: Option<WhereClause>,
14
+
pub sort_by: Option<Vec<SortField>>,
15
+
}
16
+
17
+
#[derive(Debug, Serialize)]
18
+
#[serde(rename_all = "camelCase")]
19
+
pub struct IndexedRecord {
20
+
pub uri: String,
21
+
pub cid: String,
22
+
pub did: String,
23
+
pub collection: String,
24
+
pub value: Value,
25
+
pub indexed_at: String,
26
+
}
27
+
28
+
#[derive(Debug, Serialize)]
29
+
#[serde(rename_all = "camelCase")]
30
+
pub struct Output {
31
+
pub records: Vec<IndexedRecord>,
32
+
pub cursor: Option<String>,
33
+
}
34
+
35
+
pub async fn handler(
36
+
State(state): State<AppState>,
37
+
Json(params): Json<Params>,
38
+
) -> Result<Json<Output>, AppError> {
39
+
let (records, cursor) = state
40
+
.database
41
+
.get_slice_collections_records(
42
+
¶ms.slice,
43
+
params.limit,
44
+
params.cursor.as_deref(),
45
+
params.sort_by.as_ref(),
46
+
params.where_clause.as_ref(),
47
+
)
48
+
.await
49
+
.map_err(|e| AppError::Internal(format!("Failed to get slice records: {}", e)))?;
50
+
51
+
let indexed_records: Vec<IndexedRecord> = records
52
+
.into_iter()
53
+
.map(|record| IndexedRecord {
54
+
uri: record.uri,
55
+
cid: record.cid,
56
+
did: record.did,
57
+
collection: record.collection,
58
+
value: record.json,
59
+
indexed_at: record.indexed_at.to_rfc3339(),
60
+
})
61
+
.collect();
62
+
63
+
Ok(Json(Output {
64
+
records: indexed_records,
65
+
cursor,
66
+
}))
67
+
}
+54
api/src/xrpc/network/slices/slice/get_sparklines.rs
+54
api/src/xrpc/network/slices/slice/get_sparklines.rs
···
1
+
use axum::{extract::State, response::Json};
2
+
use serde::{Deserialize, Serialize};
3
+
use crate::{AppState, errors::AppError, models::SparklinePoint};
4
+
5
+
#[derive(Debug, Deserialize)]
6
+
#[serde(rename_all = "camelCase")]
7
+
pub struct Params {
8
+
pub slices: Vec<String>,
9
+
pub interval: Option<String>,
10
+
pub duration: Option<String>,
11
+
}
12
+
13
+
#[derive(Debug, Serialize)]
14
+
#[serde(rename_all = "camelCase")]
15
+
pub struct SparklineEntry {
16
+
pub slice_uri: String,
17
+
pub points: Vec<SparklinePoint>,
18
+
}
19
+
20
+
#[derive(Debug, Serialize)]
21
+
#[serde(rename_all = "camelCase")]
22
+
pub struct Output {
23
+
pub sparklines: Vec<SparklineEntry>,
24
+
}
25
+
26
+
pub async fn handler(
27
+
State(state): State<AppState>,
28
+
Json(params): Json<Params>,
29
+
) -> Result<Json<Output>, AppError> {
30
+
let interval = params.interval.as_deref().unwrap_or("hour");
31
+
let duration = params.duration.as_deref().unwrap_or("24h");
32
+
33
+
let duration_hours = match duration {
34
+
"1h" => 1,
35
+
"24h" => 24,
36
+
"7d" => 24 * 7,
37
+
"30d" => 24 * 30,
38
+
_ => 24,
39
+
};
40
+
41
+
let sparklines_map = state
42
+
.database
43
+
.get_batch_sparkline_data(¶ms.slices, interval, duration_hours)
44
+
.await
45
+
.map_err(|e| AppError::Internal(format!("Failed to get sparkline data: {}", e)))?;
46
+
47
+
// Convert HashMap to array of SparklineEntry
48
+
let sparklines = sparklines_map
49
+
.into_iter()
50
+
.map(|(slice_uri, points)| SparklineEntry { slice_uri, points })
51
+
.collect();
52
+
53
+
Ok(Json(Output { sparklines }))
54
+
}
+16
api/src/xrpc/network/slices/slice/mod.rs
+16
api/src/xrpc/network/slices/slice/mod.rs
···
1
+
pub mod create_oauth_client;
2
+
pub mod delete_oauth_client;
3
+
pub mod get_actors;
4
+
pub mod get_jetstream_logs;
5
+
pub mod get_jetstream_status;
6
+
pub mod get_job_history;
7
+
pub mod get_job_logs;
8
+
pub mod get_job_status;
9
+
pub mod get_oauth_clients;
10
+
pub mod get_slice_records;
11
+
pub mod get_sparklines;
12
+
pub mod openapi;
13
+
pub mod start_sync;
14
+
pub mod stats;
15
+
pub mod sync_user_collections;
16
+
pub mod update_oauth_client;
+1
api/src/xrpc/network/slices/slice/openapi.rs
+1
api/src/xrpc/network/slices/slice/openapi.rs
···
1
+
pub use crate::api::openapi::get_openapi_spec as handler;
+40
api/src/xrpc/network/slices/slice/start_sync.rs
+40
api/src/xrpc/network/slices/slice/start_sync.rs
···
1
+
use axum::{extract::State, http::HeaderMap, response::Json};
2
+
use serde::{Deserialize, Serialize};
3
+
use uuid::Uuid;
4
+
use crate::{AppState, auth, errors::AppError, jobs, models::BulkSyncParams};
5
+
6
+
#[derive(Debug, Deserialize)]
7
+
#[serde(rename_all = "camelCase")]
8
+
pub struct Params {
9
+
#[serde(flatten)]
10
+
pub sync_params: BulkSyncParams,
11
+
pub slice: String,
12
+
}
13
+
14
+
#[derive(Debug, Serialize)]
15
+
#[serde(rename_all = "camelCase")]
16
+
pub struct Output {
17
+
pub job_id: Uuid,
18
+
pub message: String,
19
+
}
20
+
21
+
pub async fn handler(
22
+
State(state): State<AppState>,
23
+
headers: HeaderMap,
24
+
Json(params): Json<Params>,
25
+
) -> Result<Json<Output>, AppError> {
26
+
let token = auth::extract_bearer_token(&headers)?;
27
+
let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
28
+
29
+
let user_did = user_info.sub;
30
+
let slice_uri = params.slice;
31
+
32
+
let job_id = jobs::enqueue_sync_job(&state.database_pool, user_did, slice_uri.clone(), params.sync_params)
33
+
.await
34
+
.map_err(|e| AppError::Internal(format!("Failed to enqueue sync job: {}", e)))?;
35
+
36
+
Ok(Json(Output {
37
+
job_id,
38
+
message: format!("Sync job {} enqueued successfully", job_id),
39
+
}))
40
+
}
+48
api/src/xrpc/network/slices/slice/stats.rs
+48
api/src/xrpc/network/slices/slice/stats.rs
···
1
+
use axum::{extract::{State, Query}, response::Json};
2
+
use serde::{Deserialize, Serialize};
3
+
use crate::{AppState, errors::AppError, models::CollectionStats};
4
+
5
+
#[derive(Debug, Deserialize)]
6
+
#[serde(rename_all = "camelCase")]
7
+
pub struct Params {
8
+
pub slice: String,
9
+
}
10
+
11
+
#[derive(Debug, Serialize)]
12
+
#[serde(rename_all = "camelCase")]
13
+
pub struct Output {
14
+
pub collections: Vec<String>,
15
+
pub collection_stats: Vec<CollectionStats>,
16
+
pub total_lexicons: i64,
17
+
pub total_records: i64,
18
+
pub total_actors: i64,
19
+
}
20
+
21
+
pub async fn handler(
22
+
State(state): State<AppState>,
23
+
Query(params): Query<Params>,
24
+
) -> Result<Json<Output>, AppError> {
25
+
let (collections, collection_stats, total_lexicons, total_records, total_actors) = tokio::try_join!(
26
+
state.database.get_slice_collections_list(¶ms.slice),
27
+
state.database.get_slice_collection_stats(¶ms.slice),
28
+
state.database.get_slice_lexicon_count(¶ms.slice),
29
+
state.database.get_slice_total_records(¶ms.slice),
30
+
state.database.get_slice_total_actors(¶ms.slice),
31
+
)
32
+
.map_err(|e| AppError::Internal(format!("Failed to get slice statistics: {}", e)))?;
33
+
34
+
Ok(Json(Output {
35
+
collections,
36
+
collection_stats: collection_stats
37
+
.into_iter()
38
+
.map(|cs| CollectionStats {
39
+
collection: cs.collection,
40
+
record_count: cs.record_count,
41
+
unique_actors: cs.unique_actors,
42
+
})
43
+
.collect(),
44
+
total_lexicons,
45
+
total_records,
46
+
total_actors,
47
+
}))
48
+
}
+60
api/src/xrpc/network/slices/slice/sync_user_collections.rs
+60
api/src/xrpc/network/slices/slice/sync_user_collections.rs
···
1
+
use axum::{extract::State, http::HeaderMap, response::Json};
2
+
use serde::Deserialize;
3
+
use crate::{AppState, auth, errors::AppError};
4
+
5
+
#[derive(Debug, Deserialize)]
6
+
#[serde(rename_all = "camelCase")]
7
+
pub struct Params {
8
+
pub slice: String,
9
+
#[serde(default = "default_timeout")]
10
+
pub timeout_seconds: u64,
11
+
}
12
+
13
+
fn default_timeout() -> u64 {
14
+
30
15
+
}
16
+
17
+
pub async fn handler(
18
+
State(state): State<AppState>,
19
+
headers: HeaderMap,
20
+
Json(params): Json<Params>,
21
+
) -> Result<Json<crate::sync::SyncUserCollectionsResult>, AppError> {
22
+
let token = auth::extract_bearer_token(&headers)?;
23
+
let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
24
+
25
+
let user_did = user_info.did.unwrap_or(user_info.sub);
26
+
27
+
if params.timeout_seconds > 300 {
28
+
return Err(AppError::BadRequest(
29
+
"Maximum timeout is 300 seconds (5 minutes)".to_string(),
30
+
));
31
+
}
32
+
33
+
tracing::info!(
34
+
"🔄 Starting user collections sync for {} on slice {} (timeout: {}s)",
35
+
user_did,
36
+
params.slice,
37
+
params.timeout_seconds
38
+
);
39
+
40
+
let sync_service =
41
+
crate::sync::SyncService::new(state.database.clone(), state.config.relay_endpoint.clone());
42
+
43
+
let result = sync_service
44
+
.sync_user_collections(&user_did, ¶ms.slice, params.timeout_seconds)
45
+
.await
46
+
.map_err(|e| AppError::Internal(format!("Sync operation failed: {}", e)))?;
47
+
48
+
if result.timed_out {
49
+
tracing::info!("⏰ Sync timed out for user {}, suggesting async job", user_did);
50
+
} else {
51
+
tracing::info!(
52
+
"✅ Sync completed for user {}: {} repos, {} records",
53
+
user_did,
54
+
result.repos_processed,
55
+
result.records_synced
56
+
);
57
+
}
58
+
59
+
Ok(Json(result))
60
+
}
+144
api/src/xrpc/network/slices/slice/update_oauth_client.rs
+144
api/src/xrpc/network/slices/slice/update_oauth_client.rs
···
1
+
use axum::{extract::State, http::HeaderMap, response::Json};
2
+
use reqwest::Client;
3
+
use serde::{Deserialize, Serialize};
4
+
use crate::{AppState, auth, errors::AppError};
5
+
6
+
#[derive(Debug, Deserialize)]
7
+
#[serde(rename_all = "camelCase")]
8
+
pub struct Params {
9
+
pub client_id: String,
10
+
pub client_name: Option<String>,
11
+
pub redirect_uris: Option<Vec<String>>,
12
+
pub scope: Option<String>,
13
+
pub client_uri: Option<String>,
14
+
pub logo_uri: Option<String>,
15
+
pub tos_uri: Option<String>,
16
+
pub policy_uri: Option<String>,
17
+
}
18
+
19
+
#[derive(Debug, Serialize)]
20
+
#[serde(rename_all = "camelCase")]
21
+
pub struct Output {
22
+
pub client_id: String,
23
+
pub client_secret: Option<String>,
24
+
pub client_name: String,
25
+
pub redirect_uris: Vec<String>,
26
+
pub grant_types: Vec<String>,
27
+
pub response_types: Vec<String>,
28
+
pub scope: Option<String>,
29
+
pub client_uri: Option<String>,
30
+
pub logo_uri: Option<String>,
31
+
pub tos_uri: Option<String>,
32
+
pub policy_uri: Option<String>,
33
+
pub created_at: chrono::DateTime<chrono::Utc>,
34
+
pub created_by_did: String,
35
+
}
36
+
37
+
#[derive(Debug, Serialize)]
38
+
#[serde(rename_all = "snake_case")]
39
+
struct AipClientRequest {
40
+
pub client_name: String,
41
+
pub redirect_uris: Vec<String>,
42
+
#[serde(skip_serializing_if = "Option::is_none")]
43
+
pub scope: Option<String>,
44
+
#[serde(skip_serializing_if = "Option::is_none")]
45
+
pub client_uri: Option<String>,
46
+
#[serde(skip_serializing_if = "Option::is_none")]
47
+
pub logo_uri: Option<String>,
48
+
#[serde(skip_serializing_if = "Option::is_none")]
49
+
pub tos_uri: Option<String>,
50
+
#[serde(skip_serializing_if = "Option::is_none")]
51
+
pub policy_uri: Option<String>,
52
+
}
53
+
54
+
#[derive(Deserialize)]
55
+
#[serde(rename_all = "snake_case")]
56
+
struct AipClientResponse {
57
+
pub client_id: String,
58
+
pub client_secret: Option<String>,
59
+
pub client_name: String,
60
+
pub redirect_uris: Vec<String>,
61
+
pub grant_types: Vec<String>,
62
+
pub response_types: Vec<String>,
63
+
pub scope: Option<String>,
64
+
pub client_uri: Option<String>,
65
+
pub logo_uri: Option<String>,
66
+
pub tos_uri: Option<String>,
67
+
pub policy_uri: Option<String>,
68
+
}
69
+
70
+
pub async fn handler(
71
+
State(state): State<AppState>,
72
+
headers: HeaderMap,
73
+
Json(params): Json<Params>,
74
+
) -> Result<Json<Output>, AppError> {
75
+
let token = auth::extract_bearer_token(&headers)?;
76
+
auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
77
+
78
+
let oauth_client = state
79
+
.database
80
+
.get_oauth_client_by_id(¶ms.client_id)
81
+
.await
82
+
.map_err(|e| AppError::Internal(format!("Failed to fetch OAuth client: {}", e)))?
83
+
.ok_or_else(|| AppError::NotFound("OAuth client not found".to_string()))?;
84
+
85
+
let registration_token = oauth_client
86
+
.registration_access_token
87
+
.ok_or_else(|| AppError::Internal("Client missing registration access token".to_string()))?;
88
+
89
+
let aip_request = AipClientRequest {
90
+
client_name: params.client_name.unwrap_or_default(),
91
+
redirect_uris: params.redirect_uris.unwrap_or_default(),
92
+
scope: params.scope,
93
+
client_uri: params.client_uri,
94
+
logo_uri: params.logo_uri,
95
+
tos_uri: params.tos_uri,
96
+
policy_uri: params.policy_uri,
97
+
};
98
+
99
+
let client = Client::new();
100
+
let update_url = format!(
101
+
"{}/oauth/clients/{}",
102
+
state.config.auth_base_url, params.client_id
103
+
);
104
+
105
+
let aip_response = client
106
+
.put(&update_url)
107
+
.bearer_auth(®istration_token)
108
+
.json(&aip_request)
109
+
.send()
110
+
.await
111
+
.map_err(|e| AppError::Internal(format!("Failed to update client with AIP: {}", e)))?;
112
+
113
+
if !aip_response.status().is_success() {
114
+
let error_text = aip_response
115
+
.text()
116
+
.await
117
+
.unwrap_or_else(|_| "Unknown error".to_string());
118
+
return Err(AppError::Internal(format!("AIP update failed: {}", error_text)));
119
+
}
120
+
121
+
let response_body = aip_response
122
+
.text()
123
+
.await
124
+
.map_err(|e| AppError::Internal(format!("Failed to get response body: {}", e)))?;
125
+
126
+
let aip_client: AipClientResponse = serde_json::from_str(&response_body)
127
+
.map_err(|e| AppError::Internal(format!("Failed to parse AIP response: {}", e)))?;
128
+
129
+
Ok(Json(Output {
130
+
client_id: aip_client.client_id,
131
+
client_secret: aip_client.client_secret,
132
+
client_name: aip_client.client_name,
133
+
redirect_uris: aip_client.redirect_uris,
134
+
grant_types: aip_client.grant_types,
135
+
response_types: aip_client.response_types,
136
+
scope: aip_client.scope,
137
+
client_uri: aip_client.client_uri,
138
+
logo_uri: aip_client.logo_uri,
139
+
tos_uri: aip_client.tos_uri,
140
+
policy_uri: aip_client.policy_uri,
141
+
created_at: oauth_client.created_at,
142
+
created_by_did: oauth_client.created_by_did,
143
+
}))
144
+
}
+74
deno.lock
+74
deno.lock
···
2
2
"version": "5",
3
3
"specifiers": {
4
4
"jsr:@shikijs/shiki@^3.7.0": "3.7.0",
5
+
"jsr:@std/assert@*": "1.0.14",
6
+
"jsr:@std/cli@^1.0.21": "1.0.22",
7
+
"jsr:@std/cli@^1.0.22": "1.0.22",
8
+
"jsr:@std/encoding@^1.0.10": "1.0.10",
9
+
"jsr:@std/fmt@^1.0.2": "1.0.8",
10
+
"jsr:@std/fmt@^1.0.8": "1.0.8",
11
+
"jsr:@std/fs@^1.0.19": "1.0.19",
12
+
"jsr:@std/fs@^1.0.4": "1.0.19",
13
+
"jsr:@std/html@^1.0.4": "1.0.4",
14
+
"jsr:@std/http@^1.0.17": "1.0.20",
15
+
"jsr:@std/internal@^1.0.10": "1.0.10",
16
+
"jsr:@std/internal@^1.0.9": "1.0.10",
17
+
"jsr:@std/media-types@^1.1.0": "1.1.0",
18
+
"jsr:@std/net@^1.0.4": "1.0.6",
19
+
"jsr:@std/path@^1.0.6": "1.1.2",
20
+
"jsr:@std/path@^1.1.1": "1.1.2",
21
+
"jsr:@std/streams@^1.0.10": "1.0.12",
5
22
"npm:@shikijs/core@^3.7.0": "3.13.0",
6
23
"npm:@shikijs/engine-oniguruma@^3.7.0": "3.13.0",
7
24
"npm:@shikijs/types@^3.7.0": "3.13.0",
···
29
46
"npm:@shikijs/types",
30
47
"npm:shiki"
31
48
]
49
+
},
50
+
"@std/assert@1.0.14": {
51
+
"integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4",
52
+
"dependencies": [
53
+
"jsr:@std/internal@^1.0.10"
54
+
]
55
+
},
56
+
"@std/cli@1.0.22": {
57
+
"integrity": "50d1e4f87887cb8a8afa29b88505ab5081188f5cad3985460c3b471fa49ff21a"
58
+
},
59
+
"@std/encoding@1.0.10": {
60
+
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
61
+
},
62
+
"@std/fmt@1.0.8": {
63
+
"integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7"
64
+
},
65
+
"@std/fs@1.0.19": {
66
+
"integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06",
67
+
"dependencies": [
68
+
"jsr:@std/internal@^1.0.9",
69
+
"jsr:@std/path@^1.1.1"
70
+
]
71
+
},
72
+
"@std/html@1.0.4": {
73
+
"integrity": "eff3497c08164e6ada49b7f81a28b5108087033823153d065e3f89467dd3d50e"
74
+
},
75
+
"@std/http@1.0.20": {
76
+
"integrity": "b5cc33fc001bccce65ed4c51815668c9891c69ccd908295997e983d8f56070a1",
77
+
"dependencies": [
78
+
"jsr:@std/cli@^1.0.21",
79
+
"jsr:@std/encoding",
80
+
"jsr:@std/fmt@^1.0.8",
81
+
"jsr:@std/fs@^1.0.19",
82
+
"jsr:@std/html",
83
+
"jsr:@std/media-types",
84
+
"jsr:@std/net",
85
+
"jsr:@std/path@^1.1.1",
86
+
"jsr:@std/streams"
87
+
]
88
+
},
89
+
"@std/internal@1.0.10": {
90
+
"integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7"
91
+
},
92
+
"@std/media-types@1.1.0": {
93
+
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
94
+
},
95
+
"@std/net@1.0.6": {
96
+
"integrity": "110735f93e95bb9feb95790a8b1d1bf69ec0dc74f3f97a00a76ea5efea25500c"
97
+
},
98
+
"@std/path@1.1.2": {
99
+
"integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038",
100
+
"dependencies": [
101
+
"jsr:@std/internal@^1.0.10"
102
+
]
103
+
},
104
+
"@std/streams@1.0.12": {
105
+
"integrity": "ae925fa1dc459b1abf5cbaa28cc5c7b0485853af3b2a384b0dc22d86e59dfbf4"
32
106
}
33
107
},
34
108
"npm": {
+401
-298
frontend/src/client.ts
+401
-298
frontend/src/client.ts
···
1
1
// Generated TypeScript client for AT Protocol records
2
-
// Generated at: 2025-09-24 17:50:48 UTC
3
-
// Lexicons: 25
2
+
// Generated at: 2025-09-26 18:40:59 UTC
3
+
// Lexicons: 40
4
4
5
5
/**
6
6
* @example Usage
···
53
53
type AuthProvider,
54
54
type BlobRef,
55
55
type CountRecordsResponse,
56
-
type GetActorsParams,
57
-
type GetActorsResponse,
58
56
type GetRecordParams,
59
57
type GetRecordsResponse,
60
58
type IndexedRecordFields,
61
59
type RecordResponse,
62
-
type SliceLevelRecordsParams,
63
-
type SliceRecordsOutput,
64
60
SlicesClient,
65
61
type SortField,
66
62
type WhereCondition,
67
63
} from "@slices/client";
68
64
import type { OAuthClient } from "@slices/oauth";
69
-
70
-
export interface BulkSyncParams {
71
-
collections?: string[];
72
-
externalCollections?: string[];
73
-
repos?: string[];
74
-
limitPerRepo?: number;
75
-
}
76
-
77
-
export interface BulkSyncOutput {
78
-
success: boolean;
79
-
totalRecords: number;
80
-
collectionsSynced: string[];
81
-
reposProcessed: number;
82
-
message: string;
83
-
}
84
-
85
-
export interface SyncJobResponse {
86
-
success: boolean;
87
-
jobId?: string;
88
-
message: string;
89
-
}
90
-
91
-
export interface SyncJobResult {
92
-
success: boolean;
93
-
totalRecords: number;
94
-
collectionsSynced: string[];
95
-
reposProcessed: number;
96
-
message: string;
97
-
}
98
-
99
-
export interface JobStatus {
100
-
jobId: string;
101
-
status: string;
102
-
createdAt: string;
103
-
startedAt?: string;
104
-
completedAt?: string;
105
-
result?: SyncJobResult;
106
-
error?: string;
107
-
retryCount: number;
108
-
}
109
-
110
-
export interface GetJobStatusParams {
111
-
jobId: string;
112
-
}
113
-
114
-
export interface GetJobHistoryParams {
115
-
userDid: string;
116
-
sliceUri: string;
117
-
limit?: number;
118
-
}
119
-
120
-
export type GetJobHistoryResponse = JobStatus[];
121
-
122
-
export interface GetJobLogsParams {
123
-
jobId: string;
124
-
limit?: number;
125
-
}
126
-
127
-
export interface GetJobLogsResponse {
128
-
logs: LogEntry[];
129
-
}
130
-
131
-
export interface GetJetstreamLogsParams {
132
-
limit?: number;
133
-
}
134
-
135
-
export interface GetJetstreamLogsResponse {
136
-
logs: LogEntry[];
137
-
}
138
-
139
-
export interface LogEntry {
140
-
id: number;
141
-
createdAt: string;
142
-
logType: string;
143
-
jobId?: string;
144
-
userDid?: string;
145
-
sliceUri?: string;
146
-
level: string;
147
-
message: string;
148
-
metadata?: Record<string, unknown>;
149
-
}
150
-
151
-
export interface SyncUserCollectionsRequest {
152
-
slice: string;
153
-
timeoutSeconds?: number;
154
-
}
155
-
156
-
export interface SyncUserCollectionsResult {
157
-
success: boolean;
158
-
reposProcessed: number;
159
-
recordsSynced: number;
160
-
timedOut: boolean;
161
-
message: string;
162
-
}
163
-
164
-
export interface JetstreamStatusResponse {
165
-
connected: boolean;
166
-
status: string;
167
-
error?: string;
168
-
}
169
-
170
-
export interface CollectionStats {
171
-
collection: string;
172
-
recordCount: number;
173
-
uniqueActors: number;
174
-
}
175
-
176
-
export interface SliceStatsParams {
177
-
slice: string;
178
-
}
179
-
180
-
export interface SliceStatsOutput {
181
-
success: boolean;
182
-
collections: string[];
183
-
collectionStats: CollectionStats[];
184
-
totalLexicons: number;
185
-
totalRecords: number;
186
-
totalActors: number;
187
-
message?: string;
188
-
}
189
-
190
-
export interface GetSparklinesParams {
191
-
slices: string[];
192
-
interval?: string;
193
-
duration?: string;
194
-
}
195
-
196
-
export interface GetSparklinesOutput {
197
-
success: boolean;
198
-
sparklines: Record<string, NetworkSlicesSliceDefs["SparklinePoint"][]>;
199
-
message?: string;
200
-
}
201
-
202
-
export interface CreateOAuthClientRequest {
203
-
clientName: string;
204
-
redirectUris: string[];
205
-
grantTypes?: string[];
206
-
responseTypes?: string[];
207
-
scope?: string;
208
-
clientUri?: string;
209
-
logoUri?: string;
210
-
tosUri?: string;
211
-
policyUri?: string;
212
-
}
213
-
214
-
export interface OAuthClientDetails {
215
-
clientId: string;
216
-
clientSecret?: string;
217
-
clientName: string;
218
-
redirectUris: string[];
219
-
grantTypes: string[];
220
-
responseTypes: string[];
221
-
scope?: string;
222
-
clientUri?: string;
223
-
logoUri?: string;
224
-
tosUri?: string;
225
-
policyUri?: string;
226
-
createdAt: string;
227
-
createdByDid: string;
228
-
}
229
-
230
-
export interface ListOAuthClientsResponse {
231
-
clients: OAuthClientDetails[];
232
-
}
233
-
234
-
export interface UpdateOAuthClientRequest {
235
-
clientId: string;
236
-
clientName?: string;
237
-
redirectUris?: string[];
238
-
scope?: string;
239
-
clientUri?: string;
240
-
logoUri?: string;
241
-
tosUri?: string;
242
-
policyUri?: string;
243
-
}
244
-
245
-
export interface DeleteOAuthClientResponse {
246
-
success: boolean;
247
-
message: string;
248
-
}
249
-
250
-
export interface OAuthOperationError {
251
-
success: false;
252
-
message: string;
253
-
}
254
65
255
66
export type AppBskyGraphDefsListPurpose =
256
67
| "app.bsky.graph.defs#modlist"
···
1144
955
| "createdAt"
1145
956
| "expiresAt";
1146
957
958
+
export interface NetworkSlicesSliceSyncUserCollectionsInput {
959
+
slice: string;
960
+
timeoutSeconds?: number;
961
+
}
962
+
963
+
export interface NetworkSlicesSliceSyncUserCollectionsOutput {
964
+
reposProcessed: number;
965
+
recordsSynced: number;
966
+
timedOut: boolean;
967
+
}
968
+
1147
969
export interface NetworkSlicesSliceDefsSliceView {
1148
970
uri: string;
1149
971
cid: string;
···
1169
991
count: number;
1170
992
}
1171
993
994
+
export interface NetworkSlicesSliceGetSparklinesInput {
995
+
slices: string[];
996
+
interval?: string;
997
+
duration?: string;
998
+
}
999
+
1000
+
export interface NetworkSlicesSliceGetSparklinesOutput {
1001
+
sparklines: NetworkSlicesSliceGetSparklines["SparklineEntry"][];
1002
+
}
1003
+
1004
+
export interface NetworkSlicesSliceGetSparklinesSparklineEntry {
1005
+
/** AT-URI of the slice */
1006
+
sliceUri: string;
1007
+
/** Array of sparkline data points */
1008
+
points: NetworkSlicesSliceDefs["SparklinePoint"][];
1009
+
}
1010
+
1011
+
export interface NetworkSlicesSliceGetJobLogsParams {
1012
+
jobId: string;
1013
+
limit?: number;
1014
+
}
1015
+
1016
+
export interface NetworkSlicesSliceGetJobLogsOutput {
1017
+
logs: NetworkSlicesSliceGetJobLogs["LogEntry"][];
1018
+
}
1019
+
1020
+
export interface NetworkSlicesSliceGetJobLogsLogEntry {
1021
+
/** Log entry ID */
1022
+
id: number;
1023
+
/** When the log entry was created */
1024
+
createdAt: string;
1025
+
/** Type of log entry */
1026
+
logType: string;
1027
+
/** UUID of related job if applicable */
1028
+
jobId?: string;
1029
+
/** DID of related user if applicable */
1030
+
userDid?: string;
1031
+
/** AT-URI of related slice if applicable */
1032
+
sliceUri?: string;
1033
+
/** Log level */
1034
+
level: string;
1035
+
/** Log message */
1036
+
message: string;
1037
+
/** Additional metadata associated with the log entry */
1038
+
metadata?: unknown;
1039
+
}
1040
+
1041
+
export interface NetworkSlicesSliceGetJetstreamStatusOutput {
1042
+
connected: boolean;
1043
+
}
1044
+
1172
1045
export interface NetworkSlicesSlice {
1173
1046
/** Name of the slice */
1174
1047
name: string;
···
1180
1053
1181
1054
export type NetworkSlicesSliceSortFields = "name" | "domain" | "createdAt";
1182
1055
1056
+
export interface NetworkSlicesSliceGetJobStatusParams {
1057
+
jobId: string;
1058
+
}
1059
+
1060
+
export type NetworkSlicesSliceGetJobStatusOutput =
1061
+
NetworkSlicesSliceGetJobStatus["JobStatus"];
1062
+
1063
+
export interface NetworkSlicesSliceGetJobStatusJobStatus {
1064
+
/** UUID of the job */
1065
+
jobId: string;
1066
+
/** Current status of the job */
1067
+
status: string;
1068
+
/** When the job was created */
1069
+
createdAt: string;
1070
+
/** When the job started executing */
1071
+
startedAt?: string;
1072
+
/** When the job completed */
1073
+
completedAt?: string;
1074
+
/** Job result if completed successfully */
1075
+
result?: NetworkSlicesSliceGetJobStatus["SyncJobResult"];
1076
+
/** Error message if job failed */
1077
+
error?: string;
1078
+
/** Number of times the job has been retried */
1079
+
retryCount: number;
1080
+
}
1081
+
1082
+
export interface NetworkSlicesSliceGetJobStatusSyncJobResult {
1083
+
/** Whether the sync job completed successfully */
1084
+
success: boolean;
1085
+
/** Total number of records synced */
1086
+
totalRecords: number;
1087
+
/** List of collection NSIDs that were synced */
1088
+
collectionsSynced: string[];
1089
+
/** Number of repositories processed */
1090
+
reposProcessed: number;
1091
+
/** Human-readable message about the job completion */
1092
+
message: string;
1093
+
}
1094
+
1095
+
export interface NetworkSlicesSliceGetActorsInput {
1096
+
slice: string;
1097
+
limit?: number;
1098
+
cursor?: string;
1099
+
where?: unknown;
1100
+
}
1101
+
1102
+
export interface NetworkSlicesSliceGetActorsOutput {
1103
+
actors: NetworkSlicesSliceGetActors["Actor"][];
1104
+
cursor?: string;
1105
+
}
1106
+
1107
+
export interface NetworkSlicesSliceGetActorsActor {
1108
+
/** Decentralized identifier of the actor */
1109
+
did: string;
1110
+
/** Human-readable handle of the actor */
1111
+
handle?: string;
1112
+
/** AT-URI of the slice this actor is indexed in */
1113
+
sliceUri: string;
1114
+
/** When this actor was indexed */
1115
+
indexedAt: string;
1116
+
}
1117
+
1118
+
export interface NetworkSlicesSliceDeleteOAuthClientInput {
1119
+
clientId: string;
1120
+
}
1121
+
1122
+
export interface NetworkSlicesSliceDeleteOAuthClientOutput {
1123
+
message: string;
1124
+
}
1125
+
1126
+
export interface NetworkSlicesSliceCreateOAuthClientInput {
1127
+
sliceUri: string;
1128
+
clientName: string;
1129
+
redirectUris: string[];
1130
+
grantTypes?: string[];
1131
+
responseTypes?: string[];
1132
+
scope?: string;
1133
+
clientUri?: string;
1134
+
logoUri?: string;
1135
+
tosUri?: string;
1136
+
policyUri?: string;
1137
+
}
1138
+
1139
+
export type NetworkSlicesSliceCreateOAuthClientOutput =
1140
+
NetworkSlicesSliceGetOAuthClients["OauthClientDetails"];
1141
+
1142
+
export interface NetworkSlicesSliceGetJetstreamLogsParams {
1143
+
slice?: string;
1144
+
limit?: number;
1145
+
}
1146
+
1147
+
export interface NetworkSlicesSliceGetJetstreamLogsOutput {
1148
+
logs: NetworkSlicesSliceGetJobLogs["LogEntry"][];
1149
+
}
1150
+
1151
+
export interface NetworkSlicesSliceGetSliceRecordsInput {
1152
+
slice: string;
1153
+
limit?: number;
1154
+
cursor?: string;
1155
+
where?: unknown;
1156
+
sortBy?: unknown;
1157
+
}
1158
+
1159
+
export interface NetworkSlicesSliceGetSliceRecordsOutput {
1160
+
records: NetworkSlicesSliceGetSliceRecords["IndexedRecord"][];
1161
+
cursor?: string;
1162
+
}
1163
+
1164
+
export interface NetworkSlicesSliceGetSliceRecordsIndexedRecord {
1165
+
/** AT-URI of the record */
1166
+
uri: string;
1167
+
/** Content identifier of the record */
1168
+
cid: string;
1169
+
/** DID of the record creator */
1170
+
did: string;
1171
+
/** NSID of the collection this record belongs to */
1172
+
collection: string;
1173
+
/** The record value/content */
1174
+
value: unknown;
1175
+
/** When this record was indexed */
1176
+
indexedAt: string;
1177
+
}
1178
+
1179
+
export interface NetworkSlicesSliceStartSyncInput {
1180
+
slice: string;
1181
+
collections?: string[];
1182
+
externalCollections?: string[];
1183
+
repos?: string[];
1184
+
limitPerRepo?: number;
1185
+
skipValidation?: boolean;
1186
+
}
1187
+
1188
+
export interface NetworkSlicesSliceStartSyncOutput {
1189
+
jobId: string;
1190
+
message: string;
1191
+
}
1192
+
1193
+
export interface NetworkSlicesSliceGetJobHistoryParams {
1194
+
userDid: string;
1195
+
sliceUri: string;
1196
+
limit?: number;
1197
+
}
1198
+
1199
+
export type NetworkSlicesSliceGetJobHistoryOutput =
1200
+
NetworkSlicesSliceGetJobStatus["JobStatus"][];
1201
+
1202
+
export interface NetworkSlicesSliceStatsParams {
1203
+
slice: string;
1204
+
}
1205
+
1206
+
export interface NetworkSlicesSliceStatsOutput {
1207
+
collections: string[];
1208
+
collectionStats: NetworkSlicesSliceStats["CollectionStats"][];
1209
+
totalLexicons: number;
1210
+
totalRecords: number;
1211
+
totalActors: number;
1212
+
}
1213
+
1214
+
export interface NetworkSlicesSliceStatsCollectionStats {
1215
+
/** Collection NSID */
1216
+
collection: string;
1217
+
/** Number of records in this collection */
1218
+
recordCount: number;
1219
+
/** Number of unique actors with records in this collection */
1220
+
uniqueActors: number;
1221
+
}
1222
+
1223
+
export interface NetworkSlicesSliceUpdateOAuthClientInput {
1224
+
clientId: string;
1225
+
clientName?: string;
1226
+
redirectUris?: string[];
1227
+
scope?: string;
1228
+
clientUri?: string;
1229
+
logoUri?: string;
1230
+
tosUri?: string;
1231
+
policyUri?: string;
1232
+
}
1233
+
1234
+
export type NetworkSlicesSliceUpdateOAuthClientOutput =
1235
+
NetworkSlicesSliceGetOAuthClients["OauthClientDetails"];
1236
+
1237
+
export interface NetworkSlicesSliceGetOAuthClientsParams {
1238
+
slice: string;
1239
+
}
1240
+
1241
+
export interface NetworkSlicesSliceGetOAuthClientsOutput {
1242
+
clients: NetworkSlicesSliceGetOAuthClients["OauthClientDetails"][];
1243
+
}
1244
+
1245
+
export interface NetworkSlicesSliceGetOAuthClientsOauthClientDetails {
1246
+
/** OAuth client ID */
1247
+
clientId: string;
1248
+
/** OAuth client secret (only returned on creation) */
1249
+
clientSecret?: string;
1250
+
/** Human-readable name of the OAuth client */
1251
+
clientName: string;
1252
+
/** Allowed redirect URIs for OAuth flow */
1253
+
redirectUris: string[];
1254
+
/** Allowed OAuth grant types */
1255
+
grantTypes: string[];
1256
+
/** Allowed OAuth response types */
1257
+
responseTypes: string[];
1258
+
/** OAuth scope */
1259
+
scope?: string;
1260
+
/** URI of the client application */
1261
+
clientUri?: string;
1262
+
/** URI of the client logo */
1263
+
logoUri?: string;
1264
+
/** URI of the terms of service */
1265
+
tosUri?: string;
1266
+
/** URI of the privacy policy */
1267
+
policyUri?: string;
1268
+
/** When the OAuth client was created */
1269
+
createdAt: string;
1270
+
/** DID of the user who created this client */
1271
+
createdByDid: string;
1272
+
}
1273
+
1183
1274
export interface NetworkSlicesLexicon {
1184
1275
/** Namespaced identifier for the lexicon */
1185
1276
nsid: string;
···
1438
1529
export interface NetworkSlicesSliceDefs {
1439
1530
readonly SliceView: NetworkSlicesSliceDefsSliceView;
1440
1531
readonly SparklinePoint: NetworkSlicesSliceDefsSparklinePoint;
1532
+
}
1533
+
1534
+
export interface NetworkSlicesSliceGetSparklines {
1535
+
readonly SparklineEntry: NetworkSlicesSliceGetSparklinesSparklineEntry;
1536
+
}
1537
+
1538
+
export interface NetworkSlicesSliceGetJobLogs {
1539
+
readonly LogEntry: NetworkSlicesSliceGetJobLogsLogEntry;
1540
+
}
1541
+
1542
+
export interface NetworkSlicesSliceGetJobStatus {
1543
+
readonly JobStatus: NetworkSlicesSliceGetJobStatusJobStatus;
1544
+
readonly SyncJobResult: NetworkSlicesSliceGetJobStatusSyncJobResult;
1545
+
}
1546
+
1547
+
export interface NetworkSlicesSliceGetActors {
1548
+
readonly Actor: NetworkSlicesSliceGetActorsActor;
1549
+
}
1550
+
1551
+
export interface NetworkSlicesSliceGetSliceRecords {
1552
+
readonly IndexedRecord: NetworkSlicesSliceGetSliceRecordsIndexedRecord;
1553
+
}
1554
+
1555
+
export interface NetworkSlicesSliceStats {
1556
+
readonly CollectionStats: NetworkSlicesSliceStatsCollectionStats;
1557
+
}
1558
+
1559
+
export interface NetworkSlicesSliceGetOAuthClients {
1560
+
readonly OauthClientDetails:
1561
+
NetworkSlicesSliceGetOAuthClientsOauthClientDetails;
1441
1562
}
1442
1563
1443
1564
export interface NetworkSlicesActorDefs {
···
2073
2194
return await this.client.deleteRecord("network.slices.slice", rkey);
2074
2195
}
2075
2196
2076
-
async stats(params: SliceStatsParams): Promise<SliceStatsOutput> {
2077
-
return await this.client.makeRequest<SliceStatsOutput>(
2078
-
"network.slices.slice.stats",
2079
-
"POST",
2080
-
params,
2081
-
);
2197
+
async syncUserCollections(
2198
+
input: NetworkSlicesSliceSyncUserCollectionsInput,
2199
+
): Promise<NetworkSlicesSliceSyncUserCollectionsOutput> {
2200
+
return await this.client.makeRequest<
2201
+
NetworkSlicesSliceSyncUserCollectionsOutput
2202
+
>("network.slices.slice.syncUserCollections", "POST", input);
2082
2203
}
2083
2204
2084
2205
async getSparklines(
2085
-
params: GetSparklinesParams,
2086
-
): Promise<GetSparklinesOutput> {
2087
-
return await this.client.makeRequest<GetSparklinesOutput>(
2206
+
input: NetworkSlicesSliceGetSparklinesInput,
2207
+
): Promise<NetworkSlicesSliceGetSparklinesOutput> {
2208
+
return await this.client.makeRequest<NetworkSlicesSliceGetSparklinesOutput>(
2088
2209
"network.slices.slice.getSparklines",
2089
2210
"POST",
2090
-
params,
2211
+
input,
2091
2212
);
2092
2213
}
2093
2214
2094
-
async getSliceRecords<T = Record<string, unknown>>(
2095
-
params: Omit<SliceLevelRecordsParams<T>, "slice">,
2096
-
): Promise<SliceRecordsOutput<T>> {
2097
-
// Combine where and orWhere into the expected backend format
2098
-
const whereClause: Record<string, unknown> = params?.where
2099
-
? { ...params.where }
2100
-
: {};
2101
-
if (params?.orWhere) {
2102
-
whereClause.$or = params.orWhere;
2103
-
}
2104
-
2105
-
const requestParams = {
2106
-
...params,
2107
-
where: Object.keys(whereClause).length > 0 ? whereClause : undefined,
2108
-
orWhere: undefined, // Remove orWhere as it's now in where.$or
2109
-
slice: this.client.sliceUri,
2110
-
};
2111
-
return await this.client.makeRequest<SliceRecordsOutput<T>>(
2112
-
"network.slices.slice.getSliceRecords",
2113
-
"POST",
2114
-
requestParams,
2115
-
);
2116
-
}
2117
-
2118
-
async getActors(params?: GetActorsParams): Promise<GetActorsResponse> {
2119
-
const requestParams = { ...params, slice: this.client.sliceUri };
2120
-
return await this.client.makeRequest<GetActorsResponse>(
2121
-
"network.slices.slice.getActors",
2122
-
"POST",
2123
-
requestParams,
2215
+
async getJobLogs(
2216
+
params?: NetworkSlicesSliceGetJobLogsParams,
2217
+
): Promise<NetworkSlicesSliceGetJobLogsOutput> {
2218
+
return await this.client.makeRequest<NetworkSlicesSliceGetJobLogsOutput>(
2219
+
"network.slices.slice.getJobLogs",
2220
+
"GET",
2221
+
params,
2124
2222
);
2125
2223
}
2126
2224
2127
-
async startSync(params: BulkSyncParams): Promise<SyncJobResponse> {
2128
-
const requestParams = { ...params, slice: this.client.sliceUri };
2129
-
return await this.client.makeRequest<SyncJobResponse>(
2130
-
"network.slices.slice.startSync",
2131
-
"POST",
2132
-
requestParams,
2133
-
);
2225
+
async getJetstreamStatus(): Promise<
2226
+
NetworkSlicesSliceGetJetstreamStatusOutput
2227
+
> {
2228
+
return await this.client.makeRequest<
2229
+
NetworkSlicesSliceGetJetstreamStatusOutput
2230
+
>("network.slices.slice.getJetstreamStatus", "GET", {});
2134
2231
}
2135
2232
2136
-
async getJobStatus(params: GetJobStatusParams): Promise<JobStatus> {
2137
-
return await this.client.makeRequest<JobStatus>(
2233
+
async getJobStatus(
2234
+
params?: NetworkSlicesSliceGetJobStatusParams,
2235
+
): Promise<NetworkSlicesSliceGetJobStatusOutput> {
2236
+
return await this.client.makeRequest<NetworkSlicesSliceGetJobStatusOutput>(
2138
2237
"network.slices.slice.getJobStatus",
2139
2238
"GET",
2140
2239
params,
2141
2240
);
2142
2241
}
2143
2242
2144
-
async getJobHistory(
2145
-
params: GetJobHistoryParams,
2146
-
): Promise<GetJobHistoryResponse> {
2147
-
return await this.client.makeRequest<GetJobHistoryResponse>(
2148
-
"network.slices.slice.getJobHistory",
2149
-
"GET",
2150
-
params,
2243
+
async getActors(
2244
+
input: NetworkSlicesSliceGetActorsInput,
2245
+
): Promise<NetworkSlicesSliceGetActorsOutput> {
2246
+
return await this.client.makeRequest<NetworkSlicesSliceGetActorsOutput>(
2247
+
"network.slices.slice.getActors",
2248
+
"POST",
2249
+
input,
2151
2250
);
2152
2251
}
2153
2252
2154
-
async getJobLogs(params: GetJobLogsParams): Promise<GetJobLogsResponse> {
2155
-
return await this.client.makeRequest<GetJobLogsResponse>(
2156
-
"network.slices.slice.getJobLogs",
2157
-
"GET",
2158
-
params,
2159
-
);
2253
+
async deleteOAuthClient(
2254
+
input: NetworkSlicesSliceDeleteOAuthClientInput,
2255
+
): Promise<NetworkSlicesSliceDeleteOAuthClientOutput> {
2256
+
return await this.client.makeRequest<
2257
+
NetworkSlicesSliceDeleteOAuthClientOutput
2258
+
>("network.slices.slice.deleteOAuthClient", "POST", input);
2160
2259
}
2161
2260
2162
-
async getJetstreamStatus(): Promise<JetstreamStatusResponse> {
2163
-
return await this.client.makeRequest<JetstreamStatusResponse>(
2164
-
"network.slices.slice.getJetstreamStatus",
2165
-
"GET",
2166
-
);
2261
+
async createOAuthClient(
2262
+
input: NetworkSlicesSliceCreateOAuthClientInput,
2263
+
): Promise<NetworkSlicesSliceCreateOAuthClientOutput> {
2264
+
return await this.client.makeRequest<
2265
+
NetworkSlicesSliceCreateOAuthClientOutput
2266
+
>("network.slices.slice.createOAuthClient", "POST", input);
2167
2267
}
2168
2268
2169
2269
async getJetstreamLogs(
2170
-
params: GetJetstreamLogsParams,
2171
-
): Promise<GetJetstreamLogsResponse> {
2172
-
const requestParams = { ...params, slice: this.client.sliceUri };
2173
-
return await this.client.makeRequest<GetJetstreamLogsResponse>(
2174
-
"network.slices.slice.getJetstreamLogs",
2175
-
"GET",
2176
-
requestParams,
2177
-
);
2270
+
params?: NetworkSlicesSliceGetJetstreamLogsParams,
2271
+
): Promise<NetworkSlicesSliceGetJetstreamLogsOutput> {
2272
+
return await this.client.makeRequest<
2273
+
NetworkSlicesSliceGetJetstreamLogsOutput
2274
+
>("network.slices.slice.getJetstreamLogs", "GET", params);
2275
+
}
2276
+
2277
+
async getSliceRecords(
2278
+
input: NetworkSlicesSliceGetSliceRecordsInput,
2279
+
): Promise<NetworkSlicesSliceGetSliceRecordsOutput> {
2280
+
return await this.client.makeRequest<
2281
+
NetworkSlicesSliceGetSliceRecordsOutput
2282
+
>("network.slices.slice.getSliceRecords", "POST", input);
2178
2283
}
2179
2284
2180
-
async syncUserCollections(
2181
-
params?: SyncUserCollectionsRequest,
2182
-
): Promise<SyncUserCollectionsResult> {
2183
-
const requestParams = { slice: this.client.sliceUri, ...params };
2184
-
return await this.client.makeRequest<SyncUserCollectionsResult>(
2185
-
"network.slices.slice.syncUserCollections",
2285
+
async startSync(
2286
+
input: NetworkSlicesSliceStartSyncInput,
2287
+
): Promise<NetworkSlicesSliceStartSyncOutput> {
2288
+
return await this.client.makeRequest<NetworkSlicesSliceStartSyncOutput>(
2289
+
"network.slices.slice.startSync",
2186
2290
"POST",
2187
-
requestParams,
2291
+
input,
2188
2292
);
2189
2293
}
2190
2294
2191
-
async createOAuthClient(
2192
-
params: CreateOAuthClientRequest,
2193
-
): Promise<OAuthClientDetails | OAuthOperationError> {
2194
-
const requestParams = { ...params, sliceUri: this.client.sliceUri };
2195
-
return await this.client.makeRequest<
2196
-
OAuthClientDetails | OAuthOperationError
2197
-
>("network.slices.slice.createOAuthClient", "POST", requestParams);
2295
+
async getJobHistory(
2296
+
params?: NetworkSlicesSliceGetJobHistoryParams,
2297
+
): Promise<NetworkSlicesSliceGetJobHistoryOutput> {
2298
+
return await this.client.makeRequest<NetworkSlicesSliceGetJobHistoryOutput>(
2299
+
"network.slices.slice.getJobHistory",
2300
+
"GET",
2301
+
params,
2302
+
);
2198
2303
}
2199
2304
2200
-
async getOAuthClients(): Promise<ListOAuthClientsResponse> {
2201
-
const requestParams = { slice: this.client.sliceUri };
2202
-
return await this.client.makeRequest<ListOAuthClientsResponse>(
2203
-
"network.slices.slice.getOAuthClients",
2305
+
async stats(
2306
+
params?: NetworkSlicesSliceStatsParams,
2307
+
): Promise<NetworkSlicesSliceStatsOutput> {
2308
+
return await this.client.makeRequest<NetworkSlicesSliceStatsOutput>(
2309
+
"network.slices.slice.stats",
2204
2310
"GET",
2205
-
requestParams,
2311
+
params,
2206
2312
);
2207
2313
}
2208
2314
2209
2315
async updateOAuthClient(
2210
-
params: UpdateOAuthClientRequest,
2211
-
): Promise<OAuthClientDetails | OAuthOperationError> {
2212
-
const requestParams = { ...params, sliceUri: this.client.sliceUri };
2316
+
input: NetworkSlicesSliceUpdateOAuthClientInput,
2317
+
): Promise<NetworkSlicesSliceUpdateOAuthClientOutput> {
2213
2318
return await this.client.makeRequest<
2214
-
OAuthClientDetails | OAuthOperationError
2215
-
>("network.slices.slice.updateOAuthClient", "POST", requestParams);
2319
+
NetworkSlicesSliceUpdateOAuthClientOutput
2320
+
>("network.slices.slice.updateOAuthClient", "POST", input);
2216
2321
}
2217
2322
2218
-
async deleteOAuthClient(
2219
-
clientId: string,
2220
-
): Promise<DeleteOAuthClientResponse> {
2221
-
return await this.client.makeRequest<DeleteOAuthClientResponse>(
2222
-
"network.slices.slice.deleteOAuthClient",
2223
-
"POST",
2224
-
{ clientId },
2225
-
);
2323
+
async getOAuthClients(
2324
+
params?: NetworkSlicesSliceGetOAuthClientsParams,
2325
+
): Promise<NetworkSlicesSliceGetOAuthClientsOutput> {
2326
+
return await this.client.makeRequest<
2327
+
NetworkSlicesSliceGetOAuthClientsOutput
2328
+
>("network.slices.slice.getOAuthClients", "GET", params);
2226
2329
}
2227
2330
}
2228
2331
+6
-2
frontend/src/features/auth/handlers.tsx
+6
-2
frontend/src/features/auth/handlers.tsx
···
210
210
// Sync external collections first to ensure actor records are populated
211
211
try {
212
212
if (userInfo?.sub) {
213
-
await sessionClient.network.slices.slice.syncUserCollections();
213
+
await sessionClient.network.slices.slice.syncUserCollections({
214
+
slice: SLICE_URI!,
215
+
});
214
216
}
215
217
} catch (error) {
216
218
console.error("Error during external collections sync:", error);
···
427
429
428
430
// Sync user collections to populate their Bluesky profile data
429
431
try {
430
-
await sessionClient.network.slices.slice.syncUserCollections();
432
+
await sessionClient.network.slices.slice.syncUserCollections({
433
+
slice: SLICE_URI!,
434
+
});
431
435
} catch (syncError) {
432
436
console.error(
433
437
"Failed to sync user collections for waitlist user:",
+1
-5
frontend/src/features/docs/handlers.tsx
+1
-5
frontend/src/features/docs/handlers.tsx
···
257
257
async function handleDocsIndex(request: Request): Promise<Response> {
258
258
const { currentUser } = await withAuth(request);
259
259
return renderHTML(
260
-
<DocsIndexPage
261
-
docs={AVAILABLE_DOCS}
262
-
categories={DOCS_CATEGORIES}
263
-
currentUser={currentUser}
264
-
/>
260
+
<DocsIndexPage categories={DOCS_CATEGORIES} currentUser={currentUser} />
265
261
);
266
262
}
267
263
+30
-10
frontend/src/features/docs/templates/DocsIndexPage.tsx
+30
-10
frontend/src/features/docs/templates/DocsIndexPage.tsx
···
14
14
}
15
15
16
16
interface DocsIndexPageProps {
17
-
docs: DocItem[];
18
17
categories: DocCategory[];
19
18
currentUser?: AuthenticatedUser;
20
19
}
21
20
22
-
export function DocsIndexPage({ docs, categories, currentUser }: DocsIndexPageProps) {
21
+
export function DocsIndexPage({ categories, currentUser }: DocsIndexPageProps) {
23
22
return (
24
23
<Layout title="Documentation - Slices" currentUser={currentUser}>
25
24
<div className="py-8 px-4 max-w-6xl mx-auto">
···
27
26
<Text as="h1" size="3xl" className="font-bold mb-4">
28
27
Slices Documentation
29
28
</Text>
30
-
<Text as="p" size="lg" variant="secondary" className="leading-relaxed">
31
-
Learn how to build AT Protocol applications with Slices. These guides cover everything from basic concepts to advanced usage patterns.
29
+
<Text
30
+
as="p"
31
+
size="lg"
32
+
variant="secondary"
33
+
className="leading-relaxed"
34
+
>
35
+
Learn how to build AT Protocol applications with Slices. These
36
+
guides cover everything from basic concepts to advanced usage
37
+
patterns.
32
38
</Text>
33
39
</div>
34
40
35
41
<div className="space-y-16">
36
42
{categories.map((category) => (
37
43
<section key={category.category}>
38
-
<Text as="h2" size="2xl" className="font-bold mb-8 text-zinc-900 dark:text-white">
44
+
<Text
45
+
as="h2"
46
+
size="2xl"
47
+
className="font-bold mb-8 text-zinc-900 dark:text-white"
48
+
>
39
49
{category.category}
40
50
</Text>
41
51
<div className="space-y-6">
42
52
{category.docs.map((doc) => (
43
-
<div key={doc.slug} className="border-b border-zinc-200 dark:border-zinc-700 pb-6">
53
+
<div
54
+
key={doc.slug}
55
+
className="border-b border-zinc-200 dark:border-zinc-700 pb-6"
56
+
>
44
57
<a
45
58
href={`/docs/${doc.slug}`}
46
59
className="block group hover:no-underline"
47
60
>
48
-
<Text as="h3" size="lg" className="font-semibold mb-3 text-blue-600 dark:text-blue-400 group-hover:text-blue-700 dark:group-hover:text-blue-300 transition-colors underline decoration-blue-600 dark:decoration-blue-400">
61
+
<Text
62
+
as="h3"
63
+
size="lg"
64
+
className="font-semibold mb-3 text-blue-600 dark:text-blue-400 group-hover:text-blue-700 dark:group-hover:text-blue-300 transition-colors underline decoration-blue-600 dark:decoration-blue-400"
65
+
>
49
66
{doc.title}
50
67
</Text>
51
-
<Text as="p" variant="secondary" className="leading-relaxed text-base">
68
+
<Text
69
+
as="p"
70
+
variant="secondary"
71
+
className="leading-relaxed text-base"
72
+
>
52
73
{doc.description}
53
74
</Text>
54
75
</a>
···
58
79
</section>
59
80
))}
60
81
</div>
61
-
62
82
</div>
63
83
</Layout>
64
84
);
65
-
}
85
+
}
+7
-12
frontend/src/features/docs/templates/DocsPage.tsx
+7
-12
frontend/src/features/docs/templates/DocsPage.tsx
···
34
34
title,
35
35
content,
36
36
headers,
37
-
docs,
38
-
categories,
39
-
currentSlug,
40
37
currentUser,
41
38
}: DocsPageProps) {
42
39
return (
43
-
<Layout
44
-
title={`${title} - Slices`}
45
-
currentUser={currentUser}
46
-
>
40
+
<Layout title={`${title} - Slices`} currentUser={currentUser}>
47
41
<div className="py-8 px-4 max-w-6xl mx-auto relative">
48
42
{/* Breadcrumb */}
49
43
<Breadcrumb
50
-
items={[
51
-
{ label: "Documentation", href: "/docs" },
52
-
{ label: title }
53
-
]}
44
+
items={[{ label: "Documentation", href: "/docs" }, { label: title }]}
54
45
/>
55
46
56
47
{/* Two-column layout */}
···
69
60
{headers.length > 0 && (
70
61
<aside className="hidden lg:flex w-64 flex-shrink-0 relative">
71
62
<div className="sticky top-1/2 -translate-y-1/2 w-64 max-h-[60vh] overflow-y-auto bg-zinc-50 dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 shadow-sm">
72
-
<Text as="h3" size="sm" className="font-semibold mb-4 text-zinc-900 dark:text-white">
63
+
<Text
64
+
as="h3"
65
+
size="sm"
66
+
className="font-semibold mb-4 text-zinc-900 dark:text-white"
67
+
>
73
68
On This Page
74
69
</Text>
75
70
<nav>
-4
frontend/src/features/slices/codegen/handlers.tsx
-4
frontend/src/features/slices/codegen/handlers.tsx
···
6
6
import { extractSliceParams } from "../../../utils/slice-params.ts";
7
7
import { SliceCodegenPage } from "./templates/SliceCodegenPage.tsx";
8
8
import { generateTypeScript } from "@slices/codegen";
9
-
import { SLICE_URI } from "../../../config.ts";
10
9
11
10
async function handleSliceCodegenPage(
12
11
req: Request,
···
57
56
});
58
57
59
58
// Generate TypeScript client using convenience function
60
-
// If this is the main slice (SLICE_URI), include slices client functionality
61
-
const excludeSlicesClient = context.sliceContext!.sliceUri !== SLICE_URI;
62
59
generatedCode = await generateTypeScript(lexicons, {
63
60
sliceUri: context.sliceContext!.sliceUri,
64
-
excludeSlicesClient,
65
61
});
66
62
} catch (e) {
67
63
console.error("Codegen error:", e);
+17
-32
frontend/src/features/slices/jetstream/handlers.tsx
+17
-32
frontend/src/features/slices/jetstream/handlers.tsx
···
13
13
import { JetstreamLogs } from "./templates/fragments/JetstreamLogs.tsx";
14
14
import { JetstreamStatus } from "./templates/fragments/JetstreamStatus.tsx";
15
15
import { JetstreamStatusDisplay } from "./templates/fragments/JetstreamStatusDisplay.tsx";
16
-
import { buildSliceUrl } from "../../../utils/slice-params.ts";
17
-
import type { LogEntry } from "../../../client.ts";
16
+
import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../client.ts";
17
+
import { buildSliceUri } from "../../../utils/at-uri.ts";
18
18
19
19
async function handleJetstreamLogs(
20
20
req: Request,
···
36
36
// Use the slice-specific client
37
37
const sliceClient = getSliceClient(context, sliceId);
38
38
39
+
// Build slice URI from the user's DID and sliceId
40
+
const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId);
41
+
39
42
// Get Jetstream logs
40
43
const result = await sliceClient.network.slices.slice.getJetstreamLogs({
44
+
slice: sliceUri,
41
45
limit: 100,
42
46
});
43
47
···
85
89
try {
86
90
// Extract parameters from query
87
91
const url = new URL(req.url);
88
-
const sliceId = url.searchParams.get("sliceId");
89
-
const handle = url.searchParams.get("handle");
90
92
const isCompact = url.searchParams.get("compact") === "true";
93
+
const sliceId = url.searchParams.get("sliceId") || undefined;
94
+
const handle = url.searchParams.get("handle") || undefined;
91
95
92
96
// Fetch jetstream status using the public client
93
97
const data = await publicClient.network.slices.slice.getJetstreamStatus();
···
99
103
);
100
104
}
101
105
102
-
// Generate jetstream URL if we have both handle and sliceId
103
-
const jetstreamUrl =
104
-
handle && sliceId
105
-
? buildSliceUrl(handle, sliceId, "jetstream")
106
-
: undefined;
107
-
108
106
// Render full version for main page
109
107
return renderHTML(
110
108
<JetstreamStatus
111
109
connected={data.connected}
112
-
status={data.status}
113
-
error={data.error}
114
-
jetstreamUrl={jetstreamUrl}
110
+
sliceId={sliceId}
111
+
handle={handle}
115
112
/>
116
113
);
117
-
} catch (error) {
114
+
} catch (_error) {
118
115
// Extract parameters for error case too
119
116
const url = new URL(req.url);
120
-
const sliceId = url.searchParams.get("sliceId");
121
-
const handle = url.searchParams.get("handle");
122
117
const isCompact = url.searchParams.get("compact") === "true";
118
+
const sliceId = url.searchParams.get("sliceId") || undefined;
119
+
const handle = url.searchParams.get("handle") || undefined;
123
120
124
121
// Render compact error version
125
122
if (isCompact) {
126
-
return renderHTML(
127
-
<JetstreamStatusDisplay connected={false} isCompact />
128
-
);
123
+
return renderHTML(<JetstreamStatusDisplay connected={false} isCompact />);
129
124
}
130
125
131
-
// Generate jetstream URL if we have both handle and sliceId
132
-
const jetstreamUrl =
133
-
handle && sliceId
134
-
? buildSliceUrl(handle, sliceId, "jetstream")
135
-
: undefined;
136
-
137
126
// Fallback to disconnected state on error for full version
138
127
return renderHTML(
139
-
<JetstreamStatus
140
-
connected={false}
141
-
status="Connection error"
142
-
error={error instanceof Error ? error.message : "Unknown error"}
143
-
jetstreamUrl={jetstreamUrl}
144
-
/>
128
+
<JetstreamStatus connected={false} sliceId={sliceId} handle={handle} />
145
129
);
146
130
}
147
131
}
···
166
150
if (accessError) return accessError;
167
151
168
152
// Fetch Jetstream logs
169
-
let logs: LogEntry[] = [];
153
+
let logs: NetworkSlicesSliceGetJobLogsLogEntry[] = [];
170
154
171
155
try {
172
156
const sliceClient = getSliceClient(authContext, sliceParams.sliceId);
173
157
174
158
const logsResult = await sliceClient.network.slices.slice.getJetstreamLogs({
159
+
slice: context.sliceContext!.sliceUri,
175
160
limit: 100,
176
161
});
177
162
logs = logsResult.logs.sort(
+2
-2
frontend/src/features/slices/jetstream/templates/JetstreamLogsPage.tsx
+2
-2
frontend/src/features/slices/jetstream/templates/JetstreamLogsPage.tsx
···
1
1
import type {
2
-
LogEntry,
3
2
NetworkSlicesSliceDefsSliceView,
3
+
NetworkSlicesSliceGetJobLogsLogEntry,
4
4
} from "../../../../client.ts";
5
5
import { SliceLogPage } from "../../shared/fragments/SliceLogPage.tsx";
6
6
import { JetstreamLogs } from "./fragments/JetstreamLogs.tsx";
···
10
10
11
11
interface JetstreamLogsPageProps {
12
12
slice: NetworkSlicesSliceDefsSliceView;
13
-
logs: LogEntry[];
13
+
logs: NetworkSlicesSliceGetJobLogsLogEntry[];
14
14
sliceId: string;
15
15
currentUser?: AuthenticatedUser;
16
16
}
+2
-2
frontend/src/features/slices/jetstream/templates/fragments/JetstreamLogs.tsx
+2
-2
frontend/src/features/slices/jetstream/templates/fragments/JetstreamLogs.tsx
···
1
-
import type { LogEntry } from "../../../../../client.ts";
2
1
import { formatTimestamp } from "../../../../../utils/time.ts";
3
2
import { LogViewer } from "../../../../../shared/fragments/LogViewer.tsx";
3
+
import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../../../client.ts";
4
4
5
5
interface JetstreamLogsProps {
6
-
logs: LogEntry[];
6
+
logs: NetworkSlicesSliceGetJobLogsLogEntry[];
7
7
}
8
8
9
9
export function JetstreamLogs({ logs }: JetstreamLogsProps) {
+39
-32
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatus.tsx
+39
-32
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatus.tsx
···
1
-
import { Button } from "../../../../../shared/fragments/Button.tsx";
2
1
import { Card } from "../../../../../shared/fragments/Card.tsx";
3
2
import { Text } from "../../../../../shared/fragments/Text.tsx";
3
+
import { Button } from "../../../../../shared/fragments/Button.tsx";
4
4
5
5
interface JetstreamStatusProps {
6
6
connected: boolean;
7
-
status: string;
8
-
error?: string;
9
-
jetstreamUrl?: string;
7
+
sliceId?: string;
8
+
handle?: string;
10
9
}
11
10
12
-
export function JetstreamStatus({
13
-
connected,
14
-
status,
15
-
error,
16
-
jetstreamUrl,
17
-
}: JetstreamStatusProps) {
11
+
export function JetstreamStatus({ connected, sliceId, handle }: JetstreamStatusProps) {
12
+
const showViewLogs = sliceId && handle;
13
+
18
14
if (connected) {
19
15
return (
20
-
<Card padding="sm" className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 mb-6">
16
+
<Card
17
+
padding="sm"
18
+
className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 mb-6"
19
+
>
21
20
<div className="flex items-center justify-between">
22
21
<div className="flex items-center">
23
-
<div className="w-3 h-3 bg-green-500 rounded-full mr-3 animate-pulse">
24
-
</div>
22
+
<div className="w-3 h-3 bg-green-500 rounded-full mr-3 animate-pulse"></div>
25
23
<div>
26
-
<Text as="h3" size="sm" variant="success" className="font-semibold block">
24
+
<Text
25
+
as="h3"
26
+
size="sm"
27
+
variant="success"
28
+
className="font-semibold block"
29
+
>
27
30
✈️ Jetstream Connected
28
31
</Text>
29
32
<Text as="p" size="xs" variant="success">
30
-
Real-time indexing active - new records are automatically
31
-
indexed
33
+
Real-time indexing active - new records are automatically indexed
32
34
</Text>
33
35
</div>
34
36
</div>
35
-
<div className="flex items-center gap-3">
36
-
{jetstreamUrl && (
37
+
{showViewLogs && (
38
+
<div className="flex items-center gap-3">
37
39
<Button
38
-
href={jetstreamUrl}
40
+
href={`/profile/${handle}/slice/${sliceId}/jetstream`}
39
41
variant="success"
40
42
size="sm"
41
43
className="whitespace-nowrap"
42
44
>
43
45
View Logs
44
46
</Button>
45
-
)}
46
-
</div>
47
+
</div>
48
+
)}
47
49
</div>
48
50
</Card>
49
51
);
50
52
} else {
51
53
return (
52
-
<Card padding="sm" className="bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 mb-6">
54
+
<Card
55
+
padding="sm"
56
+
className="bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 mb-6"
57
+
>
53
58
<div className="flex items-center justify-between">
54
59
<div className="flex items-center">
55
60
<div className="w-3 h-3 bg-red-500 rounded-full mr-3"></div>
56
61
<div>
57
-
<Text as="h3" size="sm" variant="error" className="font-semibold block">
62
+
<Text
63
+
as="h3"
64
+
size="sm"
65
+
variant="error"
66
+
className="font-semibold block"
67
+
>
58
68
🌊 Jetstream Disconnected
59
69
</Text>
60
70
<Text as="p" size="xs" variant="error">
61
-
Real-time indexing not active - {status}
71
+
Real-time indexing not active
62
72
</Text>
63
-
{error && (
64
-
<Text as="p" size="xs" variant="error" className="mt-1">Error: {error}</Text>
65
-
)}
66
73
</div>
67
74
</div>
68
-
<div className="flex items-center gap-3">
69
-
{jetstreamUrl && (
75
+
{showViewLogs && (
76
+
<div className="flex items-center gap-3">
70
77
<Button
71
-
href={jetstreamUrl}
78
+
href={`/profile/${handle}/slice/${sliceId}/jetstream`}
72
79
variant="danger"
73
80
size="sm"
74
81
className="whitespace-nowrap"
75
82
>
76
83
View Logs
77
84
</Button>
78
-
)}
79
-
</div>
85
+
</div>
86
+
)}
80
87
</div>
81
88
</Card>
82
89
);
+51
-78
frontend/src/features/slices/oauth/handlers.tsx
+51
-78
frontend/src/features/slices/oauth/handlers.tsx
···
6
6
requireSliceAccess,
7
7
withSliceAccess,
8
8
} from "../../../routes/slice-middleware.ts";
9
-
import {
10
-
extractSliceParams,
11
-
} from "../../../utils/slice-params.ts";
9
+
import { extractSliceParams } from "../../../utils/slice-params.ts";
12
10
import { renderHTML } from "../../../utils/render.tsx";
13
11
import { SliceOAuthPage } from "./templates/SliceOAuthPage.tsx";
14
12
import { OAuthClientModal } from "./templates/fragments/OAuthClientModal.tsx";
···
32
30
sliceUri={sliceUri}
33
31
mode="new"
34
32
clientData={undefined}
35
-
/>,
33
+
/>
36
34
);
37
35
} catch (error) {
38
36
console.error("Error showing new OAuth client modal:", error);
···
52
50
Close
53
51
</button>
54
52
</div>,
55
-
{ status: 500 },
53
+
{ status: 500 }
56
54
);
57
55
}
58
56
}
···
81
79
.map((uri) => uri.trim())
82
80
.filter((uri) => uri.length > 0);
83
81
84
-
// Register new OAuth client via backend API
85
82
const sliceClient = getSliceClient(context, sliceId);
86
-
const result = await sliceClient.network.slices.slice.createOAuthClient({
83
+
await sliceClient.network.slices.slice.createOAuthClient({
84
+
sliceUri: buildAtUri({
85
+
did: context.currentUser.sub!,
86
+
collection: "network.slices.slice",
87
+
rkey: sliceId,
88
+
}),
87
89
clientName,
88
90
redirectUris,
89
91
scope: scope || undefined,
···
93
95
policyUri: policyUri || undefined,
94
96
});
95
97
96
-
// Check if the result indicates success/failure
97
-
if (typeof result === 'object' && 'success' in result && result.success === false) {
98
-
return renderHTML(
99
-
<OAuthResult
100
-
success={false}
101
-
message={result.message || "OAuth client registration failed"}
102
-
/>
103
-
);
104
-
}
105
-
106
98
return renderHTML(
107
-
<OAuthResult
108
-
success
109
-
message="OAuth client registered successfully"
110
-
/>
99
+
<OAuthResult success message="OAuth client registered successfully" />
111
100
);
112
101
} catch (error) {
113
102
console.error("Error registering OAuth client:", error);
···
125
114
// If we can't parse JSON, try to extract from the error message
126
115
const errorStr = error.message;
127
116
if (errorStr.includes("Invalid redirect URI")) {
128
-
errorMessage = "Invalid redirect URI format. Please ensure all redirect URIs use proper HTTP/HTTPS format.";
117
+
errorMessage =
118
+
"Invalid redirect URI format. Please ensure all redirect URIs use proper HTTP/HTTPS format.";
129
119
} else if (errorStr.includes("Bad Request")) {
130
120
errorMessage = errorStr;
131
121
}
132
122
}
133
123
}
134
124
135
-
return renderHTML(
136
-
<OAuthResult
137
-
success={false}
138
-
message={errorMessage}
139
-
/>
140
-
);
125
+
return renderHTML(<OAuthResult success={false} message={errorMessage} />);
141
126
}
142
127
}
143
128
···
154
139
try {
155
140
// Delete OAuth client via backend API
156
141
const sliceClient = getSliceClient(context, sliceId);
157
-
await sliceClient.network.slices.slice.deleteOAuthClient(clientId);
142
+
await sliceClient.network.slices.slice.deleteOAuthClient({
143
+
clientId,
144
+
});
158
145
159
146
return renderHTML(<OAuthDeleteResult success />);
160
147
} catch (error) {
···
164
151
success={false}
165
152
error={error instanceof Error ? error.message : String(error)}
166
153
/>,
167
-
{ status: 500 },
154
+
{ status: 500 }
168
155
);
169
156
}
170
157
}
···
180
167
const clientId = decodeURIComponent(pathParts[5]);
181
168
182
169
try {
183
-
// Fetch OAuth client details via backend API
184
-
const sliceClient = getSliceClient(context, sliceId);
185
-
const clientsResponse = await sliceClient.network.slices.slice
186
-
.getOAuthClients();
187
-
const clientData = clientsResponse.clients.find(
188
-
(c) => c.clientId === clientId,
189
-
);
190
-
170
+
// Construct sliceUri first
191
171
const sliceUri = buildAtUri({
192
172
did: context.currentUser.sub!,
193
173
collection: "network.slices.slice",
194
174
rkey: sliceId,
195
175
});
196
176
177
+
// Fetch OAuth client details via backend API
178
+
const sliceClient = getSliceClient(context, sliceId);
179
+
const clientsResponse =
180
+
await sliceClient.network.slices.slice.getOAuthClients({
181
+
slice: sliceUri,
182
+
});
183
+
const clientData = clientsResponse.clients.find(
184
+
(c) => c.clientId === clientId
185
+
);
186
+
197
187
return renderHTML(
198
188
<OAuthClientModal
199
189
sliceId={sliceId}
200
190
sliceUri={sliceUri}
201
191
mode="view"
202
192
clientData={clientData}
203
-
/>,
193
+
/>
204
194
);
205
195
} catch (error) {
206
196
console.error("Error fetching OAuth client:", error);
···
220
210
Close
221
211
</button>
222
212
</div>,
223
-
{ status: 500 },
213
+
{ status: 500 }
224
214
);
225
215
}
226
216
}
···
251
241
.map((uri) => uri.trim())
252
242
.filter((uri) => uri.length > 0);
253
243
254
-
// Update OAuth client via backend API
255
244
const sliceClient = getSliceClient(context, sliceId);
256
-
const result = await sliceClient.network.slices.slice
257
-
.updateOAuthClient({
258
-
clientId,
259
-
clientName: clientName || undefined,
260
-
redirectUris: redirectUris.length > 0 ? redirectUris : undefined,
261
-
scope: scope || undefined,
262
-
clientUri: clientUri || undefined,
263
-
logoUri: logoUri || undefined,
264
-
tosUri: tosUri || undefined,
265
-
policyUri: policyUri || undefined,
266
-
});
267
-
268
-
// Check if the result indicates success/failure
269
-
if (typeof result === 'object' && 'success' in result && result.success === false) {
270
-
return renderHTML(
271
-
<OAuthResult
272
-
success={false}
273
-
message={result.message || "OAuth client update failed"}
274
-
/>
275
-
);
276
-
}
245
+
await sliceClient.network.slices.slice.updateOAuthClient({
246
+
clientId,
247
+
clientName: clientName || undefined,
248
+
redirectUris: redirectUris.length > 0 ? redirectUris : undefined,
249
+
scope: scope || undefined,
250
+
clientUri: clientUri || undefined,
251
+
logoUri: logoUri || undefined,
252
+
tosUri: tosUri || undefined,
253
+
policyUri: policyUri || undefined,
254
+
});
277
255
278
256
return renderHTML(
279
-
<OAuthResult
280
-
success
281
-
message="OAuth client updated successfully"
282
-
/>
257
+
<OAuthResult success message="OAuth client updated successfully" />
283
258
);
284
259
} catch (error) {
285
260
console.error("Error updating OAuth client:", error);
···
297
272
// If we can't parse JSON, try to extract from the error message
298
273
const errorStr = error.message;
299
274
if (errorStr.includes("Invalid redirect URI")) {
300
-
errorMessage = "Invalid redirect URI format. Please ensure all redirect URIs use proper HTTP/HTTPS format.";
275
+
errorMessage =
276
+
"Invalid redirect URI format. Please ensure all redirect URIs use proper HTTP/HTTPS format.";
301
277
} else if (errorStr.includes("Bad Request")) {
302
278
errorMessage = errorStr;
303
279
}
304
280
}
305
281
}
306
282
307
-
return renderHTML(
308
-
<OAuthResult
309
-
success={false}
310
-
message={errorMessage}
311
-
/>
312
-
);
283
+
return renderHTML(<OAuthResult success={false} message={errorMessage} />);
313
284
}
314
285
}
315
286
316
287
async function handleSliceOAuthPage(
317
288
req: Request,
318
-
params?: URLPatternResult,
289
+
params?: URLPatternResult
319
290
): Promise<Response> {
320
291
const authContext = await withAuth(req);
321
292
const sliceParams = extractSliceParams(params);
···
327
298
const context = await withSliceAccess(
328
299
authContext,
329
300
sliceParams.handle,
330
-
sliceParams.sliceId,
301
+
sliceParams.sliceId
331
302
);
332
303
const accessError = requireSliceAccess(context);
333
304
if (accessError) return accessError;
···
344
315
let errorMessage = null;
345
316
346
317
try {
347
-
const oauthClientsResponse = await sliceClient.network.slices.slice
348
-
.getOAuthClients();
318
+
const oauthClientsResponse =
319
+
await sliceClient.network.slices.slice.getOAuthClients({
320
+
slice: context.sliceContext!.sliceUri,
321
+
});
349
322
clientsWithDetails = oauthClientsResponse.clients.map((client) => ({
350
323
clientId: client.clientId,
351
324
createdAt: new Date().toISOString(), // Backend should provide this
···
365
338
currentUser={authContext.currentUser}
366
339
error={errorMessage}
367
340
hasSliceAccess={context.sliceContext?.hasAccess}
368
-
/>,
341
+
/>
369
342
);
370
343
}
371
344
+2
-2
frontend/src/features/slices/oauth/templates/fragments/OAuthClientModal.tsx
+2
-2
frontend/src/features/slices/oauth/templates/fragments/OAuthClientModal.tsx
···
1
-
import { OAuthClientDetails } from "../../../../../client.ts";
2
1
import { Button } from "../../../../../shared/fragments/Button.tsx";
3
2
import { Input } from "../../../../../shared/fragments/Input.tsx";
4
3
import { Textarea } from "../../../../../shared/fragments/Textarea.tsx";
5
4
import { Modal } from "../../../../../shared/fragments/Modal.tsx";
6
5
import { Text } from "../../../../../shared/fragments/Text.tsx";
6
+
import { NetworkSlicesSliceGetOAuthClientsOauthClientDetails } from "../../../../../client.ts";
7
7
8
8
interface OAuthClientModalProps {
9
9
sliceId: string;
10
10
sliceUri: string;
11
11
mode: "new" | "view";
12
-
clientData?: OAuthClientDetails;
12
+
clientData?: NetworkSlicesSliceGetOAuthClientsOauthClientDetails;
13
13
}
14
14
15
15
export function OAuthClientModal({
+14
-13
frontend/src/features/slices/records/handlers.tsx
+14
-13
frontend/src/features/slices/records/handlers.tsx
···
8
8
withSliceAccess,
9
9
} from "../../../routes/slice-middleware.ts";
10
10
import { extractSliceParams } from "../../../utils/slice-params.ts";
11
-
import type { IndexedRecord } from "@slices/client";
11
+
import type { NetworkSlicesSliceGetSliceRecordsIndexedRecord } from "../../../client.ts";
12
12
import { RecordsList } from "./templates/fragments/RecordsList.tsx";
13
13
import { Card } from "../../../shared/fragments/Card.tsx";
14
14
import { EmptyState } from "../../../shared/fragments/EmptyState.tsx";
···
50
50
const searchQuery = url.searchParams.get("search") || "";
51
51
52
52
// Fetch real records if a collection is selected
53
-
let records: Array<IndexedRecord & { pretty_value: string }> = [];
53
+
let records: Array<
54
+
NetworkSlicesSliceGetSliceRecordsIndexedRecord & { pretty_value: string }
55
+
> = [];
54
56
55
57
if (
56
58
(selectedCollection || (searchQuery && searchQuery.trim() !== "")) &&
···
64
66
);
65
67
const recordsResult =
66
68
await sliceClient.network.slices.slice.getSliceRecords({
69
+
slice: context.sliceContext.sliceUri,
67
70
where: {
68
71
...(selectedCollection && {
69
72
collection: { eq: selectedCollection },
···
75
78
limit: 20,
76
79
});
77
80
78
-
if (recordsResult.success) {
79
-
records = recordsResult.records.map((record) => ({
80
-
uri: record.uri,
81
-
indexedAt: record.indexedAt,
82
-
collection: record.collection,
83
-
did: record.did,
84
-
cid: record.cid,
85
-
value: record.value,
86
-
pretty_value: JSON.stringify(record.value, null, 2),
87
-
}));
88
-
}
81
+
records = recordsResult.records.map((record) => ({
82
+
uri: record.uri,
83
+
indexedAt: record.indexedAt,
84
+
collection: record.collection,
85
+
did: record.did,
86
+
cid: record.cid,
87
+
value: record.value,
88
+
pretty_value: JSON.stringify(record.value, null, 2),
89
+
}));
89
90
} catch (error) {
90
91
console.error("Failed to fetch records:", error);
91
92
}
+2
-3
frontend/src/features/slices/records/templates/SliceRecordsPage.tsx
+2
-3
frontend/src/features/slices/records/templates/SliceRecordsPage.tsx
···
7
7
import { Database } from "lucide-preact";
8
8
import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts";
9
9
import type { AuthenticatedUser } from "../../../../routes/middleware.ts";
10
-
import type { NetworkSlicesSliceDefsSliceView } from "../../../../client.ts";
11
-
import { IndexedRecord } from "@slices/client";
10
+
import type { NetworkSlicesSliceDefsSliceView, NetworkSlicesSliceGetSliceRecordsIndexedRecord } from "../../../../client.ts";
12
11
13
-
interface Record extends IndexedRecord {
12
+
interface Record extends NetworkSlicesSliceGetSliceRecordsIndexedRecord {
14
13
pretty_value?: string;
15
14
}
16
15
+2
-2
frontend/src/features/slices/records/templates/fragments/RecordsList.tsx
+2
-2
frontend/src/features/slices/records/templates/fragments/RecordsList.tsx
···
1
-
import { IndexedRecord } from "@slices/client";
2
1
import { Card } from "../../../../../shared/fragments/Card.tsx";
3
2
import { Text } from "../../../../../shared/fragments/Text.tsx";
3
+
import type { NetworkSlicesSliceGetSliceRecordsIndexedRecord } from "../../../../../client.ts";
4
4
5
-
interface Record extends IndexedRecord {
5
+
interface Record extends NetworkSlicesSliceGetSliceRecordsIndexedRecord {
6
6
pretty_value?: string;
7
7
}
8
8
+2
-2
frontend/src/features/slices/sync-logs/templates/SyncJobLogs.tsx
+2
-2
frontend/src/features/slices/sync-logs/templates/SyncJobLogs.tsx
···
1
-
import type { LogEntry } from "../../../../client.ts";
1
+
import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../../client.ts";
2
2
import { LogViewer } from "../../../../shared/fragments/LogViewer.tsx";
3
3
4
4
interface SyncJobLogsProps {
5
-
logs: LogEntry[];
5
+
logs: NetworkSlicesSliceGetJobLogsLogEntry[];
6
6
}
7
7
8
8
export function SyncJobLogs({ logs }: SyncJobLogsProps) {
+8
-19
frontend/src/features/slices/sync/handlers.tsx
+8
-19
frontend/src/features/slices/sync/handlers.tsx
···
64
64
}
65
65
66
66
const sliceClient = getSliceClient(context, sliceId);
67
-
const syncJobResponse = await sliceClient.network.slices.slice.startSync({
67
+
await sliceClient.network.slices.slice.startSync({
68
+
slice: buildSliceUri(context.currentUser.sub!, sliceId),
68
69
collections: collections.length > 0 ? collections : undefined,
69
70
externalCollections:
70
71
externalCollections.length > 0 ? externalCollections : undefined,
71
72
repos: repos.length > 0 ? repos : undefined,
72
73
});
73
74
74
-
if (syncJobResponse.success) {
75
-
// Get the user's handle for the redirect
76
-
const handle = context.currentUser?.handle;
77
-
if (!handle) {
78
-
throw new Error("Unable to determine user handle");
79
-
}
75
+
const handle = context.currentUser?.handle;
76
+
if (!handle) {
77
+
throw new Error("Unable to determine user handle");
78
+
}
80
79
81
-
// Redirect to the sync page to show the job started
82
-
const redirectUrl = buildSliceUrl(handle, sliceId, "sync");
83
-
return hxRedirect(redirectUrl);
84
-
} else {
85
-
return renderHTML(
86
-
<SyncResult
87
-
success={false}
88
-
message={syncJobResponse.message}
89
-
error={syncJobResponse.message}
90
-
/>
91
-
);
92
-
}
80
+
const redirectUrl = buildSliceUrl(handle, sliceId, "sync");
81
+
return hxRedirect(redirectUrl);
93
82
} catch (error) {
94
83
console.error("Failed to start sync:", error);
95
84
const errorMessage = error instanceof Error ? error.message : String(error);
+2
-18
frontend/src/features/slices/sync/templates/fragments/JobHistory.tsx
+2
-18
frontend/src/features/slices/sync/templates/fragments/JobHistory.tsx
···
5
5
import { Text } from "../../../../../shared/fragments/Text.tsx";
6
6
import { timeAgo } from "../../../../../utils/time.ts";
7
7
import { buildSliceUrl } from "../../../../../utils/slice-params.ts";
8
-
9
-
interface JobResult {
10
-
success: boolean;
11
-
totalRecords: number;
12
-
collectionsSynced: string[];
13
-
reposProcessed: number;
14
-
message: string;
15
-
}
16
-
17
-
interface JobHistoryItem {
18
-
jobId: string;
19
-
status: string;
20
-
createdAt: string;
21
-
completedAt?: string;
22
-
result?: JobResult;
23
-
error?: string;
24
-
}
8
+
import { NetworkSlicesSliceGetJobHistoryOutput } from "../../../../../client.ts";
25
9
26
10
interface JobHistoryProps {
27
-
jobs: JobHistoryItem[];
11
+
jobs: NetworkSlicesSliceGetJobHistoryOutput;
28
12
sliceId: string;
29
13
handle?: string;
30
14
}
+6
-4
frontend/src/features/slices/waitlist/api.ts
+6
-4
frontend/src/features/slices/waitlist/api.ts
···
85
85
try {
86
86
// Fetch actors to get handles
87
87
const actorsResponse = await client.network.slices.slice.getActors({
88
+
slice: sliceUri,
88
89
where: {
89
-
did: { in: dids }
90
-
}
90
+
did: { in: dids },
91
+
},
91
92
});
92
93
93
94
// Create a map of DIDs to handles
···
154
155
try {
155
156
// Fetch actors to get handles
156
157
const actorsResponse = await client.network.slices.slice.getActors({
158
+
slice: sliceUri,
157
159
where: {
158
-
did: { in: dids }
159
-
}
160
+
did: { in: dids },
161
+
},
160
162
});
161
163
162
164
// Create a map of DIDs to handles
+12
-10
frontend/src/lib/api.ts
+12
-10
frontend/src/lib/api.ts
···
5
5
NetworkSlicesSlice,
6
6
NetworkSlicesSliceDefsSliceView,
7
7
NetworkSlicesSliceDefsSparklinePoint,
8
-
SliceStatsOutput,
8
+
NetworkSlicesSliceStatsOutput,
9
9
} from "../client.ts";
10
10
import { recordBlobToCdnUrl, RecordResponse } from "@slices/client";
11
11
···
20
20
slices: sliceUris,
21
21
duration: "24h",
22
22
});
23
-
if (sparklinesResponse.success) {
24
-
return sparklinesResponse.sparklines;
23
+
24
+
// Convert array of sparklineEntry objects to a map
25
+
const sparklinesMap: Record<string, NetworkSlicesSliceDefsSparklinePoint[]> = {};
26
+
for (const entry of sparklinesResponse.sparklines) {
27
+
sparklinesMap[entry.sliceUri] = entry.points;
25
28
}
29
+
return sparklinesMap;
26
30
} catch (error) {
27
31
console.warn("Failed to fetch batch sparkline data:", error);
28
32
}
···
32
36
async function fetchStatsForSlice(
33
37
client: AtProtoClient,
34
38
sliceUri: string
35
-
): Promise<SliceStatsOutput | null> {
39
+
): Promise<NetworkSlicesSliceStatsOutput | null> {
36
40
try {
37
41
const statsResponse = await client.network.slices.slice.stats({
38
42
slice: sliceUri,
39
43
});
40
-
if (statsResponse.success) {
41
-
return statsResponse;
42
-
}
44
+
return statsResponse;
43
45
} catch (error) {
44
46
console.warn("Failed to fetch stats for slice:", sliceUri, error);
45
47
}
···
49
51
async function fetchStatsForSlices(
50
52
client: AtProtoClient,
51
53
sliceUris: string[]
52
-
): Promise<Record<string, SliceStatsOutput | null>> {
53
-
const statsMap: Record<string, SliceStatsOutput | null> = {};
54
+
): Promise<Record<string, NetworkSlicesSliceStatsOutput | null>> {
55
+
const statsMap: Record<string, NetworkSlicesSliceStatsOutput | null> = {};
54
56
55
57
// Fetch stats for each slice individually (no batch stats endpoint yet)
56
58
await Promise.all(
···
118
120
sliceRecord: RecordResponse<NetworkSlicesSlice>,
119
121
creator: NetworkSlicesActorDefsProfileViewBasic,
120
122
sparkline?: NetworkSlicesSliceDefsSparklinePoint[],
121
-
stats?: SliceStatsOutput | null
123
+
stats?: NetworkSlicesSliceStatsOutput | null
122
124
): NetworkSlicesSliceDefsSliceView {
123
125
return {
124
126
uri: sliceRecord.uri,
-4
frontend/src/routes/slice-middleware.ts
-4
frontend/src/routes/slice-middleware.ts
+82
lexicons/network/slices/slice/createOAuthClient.json
+82
lexicons/network/slices/slice/createOAuthClient.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.createOAuthClient",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Register a new OAuth client for a slice. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["sliceUri", "clientName", "redirectUris"],
13
+
"properties": {
14
+
"sliceUri": {
15
+
"type": "string",
16
+
"description": "AT-URI of the slice to register the OAuth client for"
17
+
},
18
+
"clientName": {
19
+
"type": "string",
20
+
"description": "Human-readable name of the OAuth client",
21
+
"maxLength": 256
22
+
},
23
+
"redirectUris": {
24
+
"type": "array",
25
+
"description": "Allowed redirect URIs for OAuth flow (must use HTTP or HTTPS)",
26
+
"minLength": 1,
27
+
"items": {
28
+
"type": "string",
29
+
"format": "uri"
30
+
}
31
+
},
32
+
"grantTypes": {
33
+
"type": "array",
34
+
"description": "OAuth grant types",
35
+
"items": {
36
+
"type": "string"
37
+
}
38
+
},
39
+
"responseTypes": {
40
+
"type": "array",
41
+
"description": "OAuth response types",
42
+
"items": {
43
+
"type": "string"
44
+
}
45
+
},
46
+
"scope": {
47
+
"type": "string",
48
+
"description": "OAuth scope"
49
+
},
50
+
"clientUri": {
51
+
"type": "string",
52
+
"format": "uri",
53
+
"description": "URI of the client application"
54
+
},
55
+
"logoUri": {
56
+
"type": "string",
57
+
"format": "uri",
58
+
"description": "URI of the client logo"
59
+
},
60
+
"tosUri": {
61
+
"type": "string",
62
+
"format": "uri",
63
+
"description": "URI of the terms of service"
64
+
},
65
+
"policyUri": {
66
+
"type": "string",
67
+
"format": "uri",
68
+
"description": "URI of the privacy policy"
69
+
}
70
+
}
71
+
}
72
+
},
73
+
"output": {
74
+
"encoding": "application/json",
75
+
"schema": {
76
+
"type": "ref",
77
+
"ref": "network.slices.slice.getOAuthClients#oauthClientDetails"
78
+
}
79
+
}
80
+
}
81
+
}
82
+
}
+36
lexicons/network/slices/slice/deleteOAuthClient.json
+36
lexicons/network/slices/slice/deleteOAuthClient.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.deleteOAuthClient",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Delete an OAuth client. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["clientId"],
13
+
"properties": {
14
+
"clientId": {
15
+
"type": "string",
16
+
"description": "OAuth client ID to delete"
17
+
}
18
+
}
19
+
}
20
+
},
21
+
"output": {
22
+
"encoding": "application/json",
23
+
"schema": {
24
+
"type": "object",
25
+
"required": ["message"],
26
+
"properties": {
27
+
"message": {
28
+
"type": "string",
29
+
"description": "Success confirmation message"
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
+83
lexicons/network/slices/slice/getActors.json
+83
lexicons/network/slices/slice/getActors.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.getActors",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Get actors (users) indexed in a slice with optional filtering and pagination.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["slice"],
13
+
"properties": {
14
+
"slice": {
15
+
"type": "string",
16
+
"description": "AT-URI of the slice to query"
17
+
},
18
+
"limit": {
19
+
"type": "integer",
20
+
"description": "Maximum number of actors to return",
21
+
"default": 50,
22
+
"minimum": 1,
23
+
"maximum": 100
24
+
},
25
+
"cursor": {
26
+
"type": "string",
27
+
"description": "Pagination cursor from previous response"
28
+
},
29
+
"where": {
30
+
"type": "unknown",
31
+
"description": "Flexible filtering conditions for querying actors"
32
+
}
33
+
}
34
+
}
35
+
},
36
+
"output": {
37
+
"encoding": "application/json",
38
+
"schema": {
39
+
"type": "object",
40
+
"required": ["actors"],
41
+
"properties": {
42
+
"actors": {
43
+
"type": "array",
44
+
"items": {
45
+
"type": "ref",
46
+
"ref": "#actor"
47
+
}
48
+
},
49
+
"cursor": {
50
+
"type": "string",
51
+
"description": "Pagination cursor for next page"
52
+
}
53
+
}
54
+
}
55
+
}
56
+
},
57
+
"actor": {
58
+
"type": "object",
59
+
"required": ["did", "sliceUri", "indexedAt"],
60
+
"properties": {
61
+
"did": {
62
+
"type": "string",
63
+
"format": "did",
64
+
"description": "Decentralized identifier of the actor"
65
+
},
66
+
"handle": {
67
+
"type": "string",
68
+
"format": "handle",
69
+
"description": "Human-readable handle of the actor"
70
+
},
71
+
"sliceUri": {
72
+
"type": "string",
73
+
"description": "AT-URI of the slice this actor is indexed in"
74
+
},
75
+
"indexedAt": {
76
+
"type": "string",
77
+
"format": "datetime",
78
+
"description": "When this actor was indexed"
79
+
}
80
+
}
81
+
}
82
+
}
83
+
}
+42
lexicons/network/slices/slice/getJetstreamLogs.json
+42
lexicons/network/slices/slice/getJetstreamLogs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.getJetstreamLogs",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get logs from the Jetstream real-time indexing service, optionally filtered by slice.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {
11
+
"slice": {
12
+
"type": "string",
13
+
"description": "Optional slice AT-URI to filter logs by"
14
+
},
15
+
"limit": {
16
+
"type": "integer",
17
+
"description": "Maximum number of log entries to return",
18
+
"default": 100,
19
+
"minimum": 1,
20
+
"maximum": 1000
21
+
}
22
+
}
23
+
},
24
+
"output": {
25
+
"encoding": "application/json",
26
+
"schema": {
27
+
"type": "object",
28
+
"required": ["logs"],
29
+
"properties": {
30
+
"logs": {
31
+
"type": "array",
32
+
"items": {
33
+
"type": "ref",
34
+
"ref": "network.slices.slice.getJobLogs#logEntry"
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
+27
lexicons/network/slices/slice/getJetstreamStatus.json
+27
lexicons/network/slices/slice/getJetstreamStatus.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.getJetstreamStatus",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get the current connection status of the Jetstream real-time indexing service.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"properties": {}
11
+
},
12
+
"output": {
13
+
"encoding": "application/json",
14
+
"schema": {
15
+
"type": "object",
16
+
"required": ["connected"],
17
+
"properties": {
18
+
"connected": {
19
+
"type": "boolean",
20
+
"description": "Whether Jetstream is currently connected and receiving events"
21
+
}
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
+42
lexicons/network/slices/slice/getJobHistory.json
+42
lexicons/network/slices/slice/getJobHistory.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.getJobHistory",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get the sync job history for a user and slice combination.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["userDid", "sliceUri"],
11
+
"properties": {
12
+
"userDid": {
13
+
"type": "string",
14
+
"format": "did",
15
+
"description": "DID of the user"
16
+
},
17
+
"sliceUri": {
18
+
"type": "string",
19
+
"description": "AT-URI of the slice"
20
+
},
21
+
"limit": {
22
+
"type": "integer",
23
+
"description": "Maximum number of jobs to return",
24
+
"default": 10,
25
+
"minimum": 1,
26
+
"maximum": 100
27
+
}
28
+
}
29
+
},
30
+
"output": {
31
+
"encoding": "application/json",
32
+
"schema": {
33
+
"type": "array",
34
+
"items": {
35
+
"type": "ref",
36
+
"ref": "network.slices.slice.getJobStatus#jobStatus"
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
+89
lexicons/network/slices/slice/getJobLogs.json
+89
lexicons/network/slices/slice/getJobLogs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.getJobLogs",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get logs for a specific sync job.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["jobId"],
11
+
"properties": {
12
+
"jobId": {
13
+
"type": "string",
14
+
"description": "UUID of the sync job"
15
+
},
16
+
"limit": {
17
+
"type": "integer",
18
+
"description": "Maximum number of log entries to return",
19
+
"default": 100,
20
+
"minimum": 1,
21
+
"maximum": 1000
22
+
}
23
+
}
24
+
},
25
+
"output": {
26
+
"encoding": "application/json",
27
+
"schema": {
28
+
"type": "object",
29
+
"required": ["logs"],
30
+
"properties": {
31
+
"logs": {
32
+
"type": "array",
33
+
"items": {
34
+
"type": "ref",
35
+
"ref": "#logEntry"
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
},
42
+
"logEntry": {
43
+
"type": "object",
44
+
"required": ["id", "createdAt", "logType", "level", "message"],
45
+
"properties": {
46
+
"id": {
47
+
"type": "integer",
48
+
"description": "Log entry ID"
49
+
},
50
+
"createdAt": {
51
+
"type": "string",
52
+
"format": "datetime",
53
+
"description": "When the log entry was created"
54
+
},
55
+
"logType": {
56
+
"type": "string",
57
+
"description": "Type of log entry",
58
+
"enum": ["sync_job", "jetstream", "system"]
59
+
},
60
+
"jobId": {
61
+
"type": "string",
62
+
"description": "UUID of related job if applicable"
63
+
},
64
+
"userDid": {
65
+
"type": "string",
66
+
"format": "did",
67
+
"description": "DID of related user if applicable"
68
+
},
69
+
"sliceUri": {
70
+
"type": "string",
71
+
"description": "AT-URI of related slice if applicable"
72
+
},
73
+
"level": {
74
+
"type": "string",
75
+
"description": "Log level",
76
+
"enum": ["debug", "info", "warn", "error"]
77
+
},
78
+
"message": {
79
+
"type": "string",
80
+
"description": "Log message"
81
+
},
82
+
"metadata": {
83
+
"type": "unknown",
84
+
"description": "Additional metadata associated with the log entry"
85
+
}
86
+
}
87
+
}
88
+
}
89
+
}
+100
lexicons/network/slices/slice/getJobStatus.json
+100
lexicons/network/slices/slice/getJobStatus.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.getJobStatus",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get the status of a sync job by its ID.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["jobId"],
11
+
"properties": {
12
+
"jobId": {
13
+
"type": "string",
14
+
"description": "UUID of the sync job"
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "ref",
22
+
"ref": "#jobStatus"
23
+
}
24
+
}
25
+
},
26
+
"jobStatus": {
27
+
"type": "object",
28
+
"required": ["jobId", "status", "createdAt", "retryCount"],
29
+
"properties": {
30
+
"jobId": {
31
+
"type": "string",
32
+
"description": "UUID of the job"
33
+
},
34
+
"status": {
35
+
"type": "string",
36
+
"description": "Current status of the job",
37
+
"enum": ["pending", "running", "completed", "failed"]
38
+
},
39
+
"createdAt": {
40
+
"type": "string",
41
+
"format": "datetime",
42
+
"description": "When the job was created"
43
+
},
44
+
"startedAt": {
45
+
"type": "string",
46
+
"format": "datetime",
47
+
"description": "When the job started executing"
48
+
},
49
+
"completedAt": {
50
+
"type": "string",
51
+
"format": "datetime",
52
+
"description": "When the job completed"
53
+
},
54
+
"result": {
55
+
"type": "ref",
56
+
"ref": "#syncJobResult",
57
+
"description": "Job result if completed successfully"
58
+
},
59
+
"error": {
60
+
"type": "string",
61
+
"description": "Error message if job failed"
62
+
},
63
+
"retryCount": {
64
+
"type": "integer",
65
+
"description": "Number of times the job has been retried"
66
+
}
67
+
}
68
+
},
69
+
"syncJobResult": {
70
+
"type": "object",
71
+
"required": ["success", "totalRecords", "collectionsSynced", "reposProcessed", "message"],
72
+
"properties": {
73
+
"success": {
74
+
"type": "boolean",
75
+
"description": "Whether the sync job completed successfully"
76
+
},
77
+
"totalRecords": {
78
+
"type": "integer",
79
+
"description": "Total number of records synced"
80
+
},
81
+
"collectionsSynced": {
82
+
"type": "array",
83
+
"description": "List of collection NSIDs that were synced",
84
+
"items": {
85
+
"type": "string",
86
+
"format": "nsid"
87
+
}
88
+
},
89
+
"reposProcessed": {
90
+
"type": "integer",
91
+
"description": "Number of repositories processed"
92
+
},
93
+
"message": {
94
+
"type": "string",
95
+
"description": "Human-readable message about the job completion"
96
+
}
97
+
}
98
+
}
99
+
}
100
+
}
+110
lexicons/network/slices/slice/getOAuthClients.json
+110
lexicons/network/slices/slice/getOAuthClients.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.getOAuthClients",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get all OAuth clients registered for a slice. Requires authentication.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["slice"],
11
+
"properties": {
12
+
"slice": {
13
+
"type": "string",
14
+
"description": "AT-URI of the slice to get OAuth clients for"
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "object",
22
+
"required": ["clients"],
23
+
"properties": {
24
+
"clients": {
25
+
"type": "array",
26
+
"items": {
27
+
"type": "ref",
28
+
"ref": "#oauthClientDetails"
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
},
35
+
"oauthClientDetails": {
36
+
"type": "object",
37
+
"required": ["clientId", "clientName", "redirectUris", "grantTypes", "responseTypes", "createdAt", "createdByDid"],
38
+
"properties": {
39
+
"clientId": {
40
+
"type": "string",
41
+
"description": "OAuth client ID"
42
+
},
43
+
"clientSecret": {
44
+
"type": "string",
45
+
"description": "OAuth client secret (only returned on creation)"
46
+
},
47
+
"clientName": {
48
+
"type": "string",
49
+
"description": "Human-readable name of the OAuth client"
50
+
},
51
+
"redirectUris": {
52
+
"type": "array",
53
+
"description": "Allowed redirect URIs for OAuth flow",
54
+
"items": {
55
+
"type": "string",
56
+
"format": "uri"
57
+
}
58
+
},
59
+
"grantTypes": {
60
+
"type": "array",
61
+
"description": "Allowed OAuth grant types",
62
+
"items": {
63
+
"type": "string"
64
+
}
65
+
},
66
+
"responseTypes": {
67
+
"type": "array",
68
+
"description": "Allowed OAuth response types",
69
+
"items": {
70
+
"type": "string"
71
+
}
72
+
},
73
+
"scope": {
74
+
"type": "string",
75
+
"description": "OAuth scope"
76
+
},
77
+
"clientUri": {
78
+
"type": "string",
79
+
"format": "uri",
80
+
"description": "URI of the client application"
81
+
},
82
+
"logoUri": {
83
+
"type": "string",
84
+
"format": "uri",
85
+
"description": "URI of the client logo"
86
+
},
87
+
"tosUri": {
88
+
"type": "string",
89
+
"format": "uri",
90
+
"description": "URI of the terms of service"
91
+
},
92
+
"policyUri": {
93
+
"type": "string",
94
+
"format": "uri",
95
+
"description": "URI of the privacy policy"
96
+
},
97
+
"createdAt": {
98
+
"type": "string",
99
+
"format": "datetime",
100
+
"description": "When the OAuth client was created"
101
+
},
102
+
"createdByDid": {
103
+
"type": "string",
104
+
"format": "did",
105
+
"description": "DID of the user who created this client"
106
+
}
107
+
}
108
+
}
109
+
}
110
+
}
+97
lexicons/network/slices/slice/getSliceRecords.json
+97
lexicons/network/slices/slice/getSliceRecords.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.getSliceRecords",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Query records across all collections in a slice with filtering, sorting, and pagination.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["slice"],
13
+
"properties": {
14
+
"slice": {
15
+
"type": "string",
16
+
"description": "AT-URI of the slice to query"
17
+
},
18
+
"limit": {
19
+
"type": "integer",
20
+
"description": "Maximum number of records to return",
21
+
"default": 50,
22
+
"minimum": 1,
23
+
"maximum": 100
24
+
},
25
+
"cursor": {
26
+
"type": "string",
27
+
"description": "Pagination cursor from previous response"
28
+
},
29
+
"where": {
30
+
"type": "unknown",
31
+
"description": "Flexible filtering conditions for querying records"
32
+
},
33
+
"sortBy": {
34
+
"type": "unknown",
35
+
"description": "Sorting configuration for result ordering"
36
+
}
37
+
}
38
+
}
39
+
},
40
+
"output": {
41
+
"encoding": "application/json",
42
+
"schema": {
43
+
"type": "object",
44
+
"required": ["records"],
45
+
"properties": {
46
+
"records": {
47
+
"type": "array",
48
+
"items": {
49
+
"type": "ref",
50
+
"ref": "#indexedRecord"
51
+
}
52
+
},
53
+
"cursor": {
54
+
"type": "string",
55
+
"description": "Pagination cursor for next page"
56
+
}
57
+
}
58
+
}
59
+
}
60
+
},
61
+
"indexedRecord": {
62
+
"type": "object",
63
+
"required": ["uri", "cid", "did", "collection", "value", "indexedAt"],
64
+
"properties": {
65
+
"uri": {
66
+
"type": "string",
67
+
"format": "at-uri",
68
+
"description": "AT-URI of the record"
69
+
},
70
+
"cid": {
71
+
"type": "string",
72
+
"format": "cid",
73
+
"description": "Content identifier of the record"
74
+
},
75
+
"did": {
76
+
"type": "string",
77
+
"format": "did",
78
+
"description": "DID of the record creator"
79
+
},
80
+
"collection": {
81
+
"type": "string",
82
+
"format": "nsid",
83
+
"description": "NSID of the collection this record belongs to"
84
+
},
85
+
"value": {
86
+
"type": "unknown",
87
+
"description": "The record value/content"
88
+
},
89
+
"indexedAt": {
90
+
"type": "string",
91
+
"format": "datetime",
92
+
"description": "When this record was indexed"
93
+
}
94
+
}
95
+
}
96
+
}
97
+
}
+73
lexicons/network/slices/slice/getSparklines.json
+73
lexicons/network/slices/slice/getSparklines.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.getSparklines",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Get time-series sparkline data for multiple slices showing record indexing activity over time.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["slices"],
13
+
"properties": {
14
+
"slices": {
15
+
"type": "array",
16
+
"description": "Array of slice AT-URIs to get sparkline data for",
17
+
"items": {
18
+
"type": "string"
19
+
}
20
+
},
21
+
"interval": {
22
+
"type": "string",
23
+
"description": "Time interval for data points",
24
+
"default": "hour",
25
+
"enum": ["minute", "hour", "day"]
26
+
},
27
+
"duration": {
28
+
"type": "string",
29
+
"description": "Time range to fetch data for",
30
+
"default": "24h",
31
+
"enum": ["1h", "24h", "7d", "30d"]
32
+
}
33
+
}
34
+
}
35
+
},
36
+
"output": {
37
+
"encoding": "application/json",
38
+
"schema": {
39
+
"type": "object",
40
+
"required": ["sparklines"],
41
+
"properties": {
42
+
"sparklines": {
43
+
"type": "array",
44
+
"description": "Array of slice sparkline data entries",
45
+
"items": {
46
+
"type": "ref",
47
+
"ref": "#sparklineEntry"
48
+
}
49
+
}
50
+
}
51
+
}
52
+
}
53
+
},
54
+
"sparklineEntry": {
55
+
"type": "object",
56
+
"required": ["sliceUri", "points"],
57
+
"properties": {
58
+
"sliceUri": {
59
+
"type": "string",
60
+
"description": "AT-URI of the slice"
61
+
},
62
+
"points": {
63
+
"type": "array",
64
+
"description": "Array of sparkline data points",
65
+
"items": {
66
+
"type": "ref",
67
+
"ref": "network.slices.slice.defs#sparklinePoint"
68
+
}
69
+
}
70
+
}
71
+
}
72
+
}
73
+
}
+73
lexicons/network/slices/slice/startSync.json
+73
lexicons/network/slices/slice/startSync.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.startSync",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Start a background sync job to index AT Protocol records into a slice. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["slice"],
13
+
"properties": {
14
+
"slice": {
15
+
"type": "string",
16
+
"description": "AT-URI of the slice to sync data into"
17
+
},
18
+
"collections": {
19
+
"type": "array",
20
+
"description": "List of collection NSIDs to sync (primary collections matching slice domain)",
21
+
"items": {
22
+
"type": "string",
23
+
"format": "nsid"
24
+
}
25
+
},
26
+
"externalCollections": {
27
+
"type": "array",
28
+
"description": "List of external collection NSIDs to sync (collections outside slice domain)",
29
+
"items": {
30
+
"type": "string",
31
+
"format": "nsid"
32
+
}
33
+
},
34
+
"repos": {
35
+
"type": "array",
36
+
"description": "List of specific repository DIDs to sync from",
37
+
"items": {
38
+
"type": "string",
39
+
"format": "did"
40
+
}
41
+
},
42
+
"limitPerRepo": {
43
+
"type": "integer",
44
+
"description": "Maximum number of records to sync per repository"
45
+
},
46
+
"skipValidation": {
47
+
"type": "boolean",
48
+
"description": "Skip lexicon validation during sync",
49
+
"default": false
50
+
}
51
+
}
52
+
}
53
+
},
54
+
"output": {
55
+
"encoding": "application/json",
56
+
"schema": {
57
+
"type": "object",
58
+
"required": ["jobId", "message"],
59
+
"properties": {
60
+
"jobId": {
61
+
"type": "string",
62
+
"description": "UUID of the enqueued sync job"
63
+
},
64
+
"message": {
65
+
"type": "string",
66
+
"description": "Success message confirming job enqueue"
67
+
}
68
+
}
69
+
}
70
+
}
71
+
}
72
+
}
73
+
}
+76
lexicons/network/slices/slice/stats.json
+76
lexicons/network/slices/slice/stats.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.stats",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"description": "Get statistics for a slice including collection counts, record counts, and actor counts.",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["slice"],
11
+
"properties": {
12
+
"slice": {
13
+
"type": "string",
14
+
"description": "AT-URI of the slice to get statistics for"
15
+
}
16
+
}
17
+
},
18
+
"output": {
19
+
"encoding": "application/json",
20
+
"schema": {
21
+
"type": "object",
22
+
"required": ["collections", "collectionStats", "totalLexicons", "totalRecords", "totalActors"],
23
+
"properties": {
24
+
"collections": {
25
+
"type": "array",
26
+
"description": "List of collection NSIDs indexed in this slice",
27
+
"items": {
28
+
"type": "string",
29
+
"format": "nsid"
30
+
}
31
+
},
32
+
"collectionStats": {
33
+
"type": "array",
34
+
"description": "Per-collection statistics",
35
+
"items": {
36
+
"type": "ref",
37
+
"ref": "#collectionStats"
38
+
}
39
+
},
40
+
"totalLexicons": {
41
+
"type": "integer",
42
+
"description": "Total number of lexicons defined for this slice"
43
+
},
44
+
"totalRecords": {
45
+
"type": "integer",
46
+
"description": "Total number of records indexed in this slice"
47
+
},
48
+
"totalActors": {
49
+
"type": "integer",
50
+
"description": "Total number of unique actors indexed in this slice"
51
+
}
52
+
}
53
+
}
54
+
}
55
+
},
56
+
"collectionStats": {
57
+
"type": "object",
58
+
"required": ["collection", "recordCount", "uniqueActors"],
59
+
"properties": {
60
+
"collection": {
61
+
"type": "string",
62
+
"format": "nsid",
63
+
"description": "Collection NSID"
64
+
},
65
+
"recordCount": {
66
+
"type": "integer",
67
+
"description": "Number of records in this collection"
68
+
},
69
+
"uniqueActors": {
70
+
"type": "integer",
71
+
"description": "Number of unique actors with records in this collection"
72
+
}
73
+
}
74
+
}
75
+
}
76
+
}
+51
lexicons/network/slices/slice/syncUserCollections.json
+51
lexicons/network/slices/slice/syncUserCollections.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.syncUserCollections",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Synchronously sync the authenticated user's collections into a slice with a configurable timeout. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["slice"],
13
+
"properties": {
14
+
"slice": {
15
+
"type": "string",
16
+
"description": "AT-URI of the slice to sync user data into"
17
+
},
18
+
"timeoutSeconds": {
19
+
"type": "integer",
20
+
"description": "Timeout in seconds for the sync operation",
21
+
"default": 30,
22
+
"minimum": 1,
23
+
"maximum": 300
24
+
}
25
+
}
26
+
}
27
+
},
28
+
"output": {
29
+
"encoding": "application/json",
30
+
"schema": {
31
+
"type": "object",
32
+
"required": ["reposProcessed", "recordsSynced", "timedOut"],
33
+
"properties": {
34
+
"reposProcessed": {
35
+
"type": "integer",
36
+
"description": "Number of repositories processed during sync"
37
+
},
38
+
"recordsSynced": {
39
+
"type": "integer",
40
+
"description": "Number of records successfully synced"
41
+
},
42
+
"timedOut": {
43
+
"type": "boolean",
44
+
"description": "Whether the sync operation exceeded the timeout"
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
50
+
}
51
+
}
+67
lexicons/network/slices/slice/updateOAuthClient.json
+67
lexicons/network/slices/slice/updateOAuthClient.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.slice.updateOAuthClient",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Update an existing OAuth client. Requires authentication.",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["clientId"],
13
+
"properties": {
14
+
"clientId": {
15
+
"type": "string",
16
+
"description": "OAuth client ID to update"
17
+
},
18
+
"clientName": {
19
+
"type": "string",
20
+
"description": "New human-readable name of the OAuth client",
21
+
"maxLength": 256
22
+
},
23
+
"redirectUris": {
24
+
"type": "array",
25
+
"description": "New allowed redirect URIs for OAuth flow",
26
+
"items": {
27
+
"type": "string",
28
+
"format": "uri"
29
+
}
30
+
},
31
+
"scope": {
32
+
"type": "string",
33
+
"description": "New OAuth scope"
34
+
},
35
+
"clientUri": {
36
+
"type": "string",
37
+
"format": "uri",
38
+
"description": "New URI of the client application"
39
+
},
40
+
"logoUri": {
41
+
"type": "string",
42
+
"format": "uri",
43
+
"description": "New URI of the client logo"
44
+
},
45
+
"tosUri": {
46
+
"type": "string",
47
+
"format": "uri",
48
+
"description": "New URI of the terms of service"
49
+
},
50
+
"policyUri": {
51
+
"type": "string",
52
+
"format": "uri",
53
+
"description": "New URI of the privacy policy"
54
+
}
55
+
}
56
+
}
57
+
},
58
+
"output": {
59
+
"encoding": "application/json",
60
+
"schema": {
61
+
"type": "ref",
62
+
"ref": "network.slices.slice.getOAuthClients#oauthClientDetails"
63
+
}
64
+
}
65
+
}
66
+
}
67
+
}
+1
-8
packages/cli/src/commands/codegen.ts
+1
-8
packages/cli/src/commands/codegen.ts
···
21
21
--lexicons <PATH> Directory containing lexicon files (default: ./lexicons or from slices.json)
22
22
--output <PATH> Output file path (default: ./generated_client.ts or from slices.json)
23
23
--slice <SLICE_URI> Target slice URI (required, or from slices.json)
24
-
--include-slices Include Slices XRPC methods
25
24
-h, --help Show this help message
26
25
27
26
EXAMPLES:
28
27
slices codegen --slice at://did:plc:example/slice
29
28
slices codegen --lexicons ./my-lexicons --output ./src/client.ts --slice at://did:plc:example/slice
30
-
slices codegen --include-slices --slice at://did:plc:example/slice
31
29
slices codegen # Uses config from slices.json
32
30
`);
33
31
}
···
69
67
const lexiconsPath = resolve(mergedConfig.lexiconPath!);
70
68
const outputPath = resolve(mergedConfig.clientOutputPath!);
71
69
const sliceUri = mergedConfig.slice!;
72
-
const excludeSlices = !args["include-slices"] as boolean;
73
70
74
71
try {
75
72
const lexiconFiles = await findLexiconFiles(lexiconsPath);
···
98
95
99
96
const generatedCode = await generateTypeScript(validLexicons, {
100
97
sliceUri,
101
-
excludeSlicesClient: excludeSlices,
102
98
});
103
99
104
100
const outputDir = dirname(outputPath);
···
107
103
await Deno.writeTextFile(outputPath, generatedCode);
108
104
109
105
logger.success(`Generated client: ${outputPath}`);
110
-
111
-
if (!excludeSlices) {
112
-
logger.info("Includes network.slices XRPC client methods");
113
-
}
106
+
logger.info("Includes XRPC client methods");
114
107
} catch (error) {
115
108
const err = error as Error;
116
109
logger.error(`Code generation failed: ${err.message}`);
+401
-298
packages/cli/src/generated_client.ts
+401
-298
packages/cli/src/generated_client.ts
···
1
1
// Generated TypeScript client for AT Protocol records
2
-
// Generated at: 2025-09-24 17:50:27 UTC
3
-
// Lexicons: 25
2
+
// Generated at: 2025-09-26 18:21:58 UTC
3
+
// Lexicons: 40
4
4
5
5
/**
6
6
* @example Usage
···
53
53
type AuthProvider,
54
54
type BlobRef,
55
55
type CountRecordsResponse,
56
-
type GetActorsParams,
57
-
type GetActorsResponse,
58
56
type GetRecordParams,
59
57
type GetRecordsResponse,
60
58
type IndexedRecordFields,
61
59
type RecordResponse,
62
-
type SliceLevelRecordsParams,
63
-
type SliceRecordsOutput,
64
60
SlicesClient,
65
61
type SortField,
66
62
type WhereCondition,
67
63
} from "@slices/client";
68
64
import type { OAuthClient } from "@slices/oauth";
69
-
70
-
export interface BulkSyncParams {
71
-
collections?: string[];
72
-
externalCollections?: string[];
73
-
repos?: string[];
74
-
limitPerRepo?: number;
75
-
}
76
-
77
-
export interface BulkSyncOutput {
78
-
success: boolean;
79
-
totalRecords: number;
80
-
collectionsSynced: string[];
81
-
reposProcessed: number;
82
-
message: string;
83
-
}
84
-
85
-
export interface SyncJobResponse {
86
-
success: boolean;
87
-
jobId?: string;
88
-
message: string;
89
-
}
90
-
91
-
export interface SyncJobResult {
92
-
success: boolean;
93
-
totalRecords: number;
94
-
collectionsSynced: string[];
95
-
reposProcessed: number;
96
-
message: string;
97
-
}
98
-
99
-
export interface JobStatus {
100
-
jobId: string;
101
-
status: string;
102
-
createdAt: string;
103
-
startedAt?: string;
104
-
completedAt?: string;
105
-
result?: SyncJobResult;
106
-
error?: string;
107
-
retryCount: number;
108
-
}
109
-
110
-
export interface GetJobStatusParams {
111
-
jobId: string;
112
-
}
113
-
114
-
export interface GetJobHistoryParams {
115
-
userDid: string;
116
-
sliceUri: string;
117
-
limit?: number;
118
-
}
119
-
120
-
export type GetJobHistoryResponse = JobStatus[];
121
-
122
-
export interface GetJobLogsParams {
123
-
jobId: string;
124
-
limit?: number;
125
-
}
126
-
127
-
export interface GetJobLogsResponse {
128
-
logs: LogEntry[];
129
-
}
130
-
131
-
export interface GetJetstreamLogsParams {
132
-
limit?: number;
133
-
}
134
-
135
-
export interface GetJetstreamLogsResponse {
136
-
logs: LogEntry[];
137
-
}
138
-
139
-
export interface LogEntry {
140
-
id: number;
141
-
createdAt: string;
142
-
logType: string;
143
-
jobId?: string;
144
-
userDid?: string;
145
-
sliceUri?: string;
146
-
level: string;
147
-
message: string;
148
-
metadata?: Record<string, unknown>;
149
-
}
150
-
151
-
export interface SyncUserCollectionsRequest {
152
-
slice: string;
153
-
timeoutSeconds?: number;
154
-
}
155
-
156
-
export interface SyncUserCollectionsResult {
157
-
success: boolean;
158
-
reposProcessed: number;
159
-
recordsSynced: number;
160
-
timedOut: boolean;
161
-
message: string;
162
-
}
163
-
164
-
export interface JetstreamStatusResponse {
165
-
connected: boolean;
166
-
status: string;
167
-
error?: string;
168
-
}
169
-
170
-
export interface CollectionStats {
171
-
collection: string;
172
-
recordCount: number;
173
-
uniqueActors: number;
174
-
}
175
-
176
-
export interface SliceStatsParams {
177
-
slice: string;
178
-
}
179
-
180
-
export interface SliceStatsOutput {
181
-
success: boolean;
182
-
collections: string[];
183
-
collectionStats: CollectionStats[];
184
-
totalLexicons: number;
185
-
totalRecords: number;
186
-
totalActors: number;
187
-
message?: string;
188
-
}
189
-
190
-
export interface GetSparklinesParams {
191
-
slices: string[];
192
-
interval?: string;
193
-
duration?: string;
194
-
}
195
-
196
-
export interface GetSparklinesOutput {
197
-
success: boolean;
198
-
sparklines: Record<string, NetworkSlicesSliceDefs["SparklinePoint"][]>;
199
-
message?: string;
200
-
}
201
-
202
-
export interface CreateOAuthClientRequest {
203
-
clientName: string;
204
-
redirectUris: string[];
205
-
grantTypes?: string[];
206
-
responseTypes?: string[];
207
-
scope?: string;
208
-
clientUri?: string;
209
-
logoUri?: string;
210
-
tosUri?: string;
211
-
policyUri?: string;
212
-
}
213
-
214
-
export interface OAuthClientDetails {
215
-
clientId: string;
216
-
clientSecret?: string;
217
-
clientName: string;
218
-
redirectUris: string[];
219
-
grantTypes: string[];
220
-
responseTypes: string[];
221
-
scope?: string;
222
-
clientUri?: string;
223
-
logoUri?: string;
224
-
tosUri?: string;
225
-
policyUri?: string;
226
-
createdAt: string;
227
-
createdByDid: string;
228
-
}
229
-
230
-
export interface ListOAuthClientsResponse {
231
-
clients: OAuthClientDetails[];
232
-
}
233
-
234
-
export interface UpdateOAuthClientRequest {
235
-
clientId: string;
236
-
clientName?: string;
237
-
redirectUris?: string[];
238
-
scope?: string;
239
-
clientUri?: string;
240
-
logoUri?: string;
241
-
tosUri?: string;
242
-
policyUri?: string;
243
-
}
244
-
245
-
export interface DeleteOAuthClientResponse {
246
-
success: boolean;
247
-
message: string;
248
-
}
249
-
250
-
export interface OAuthOperationError {
251
-
success: false;
252
-
message: string;
253
-
}
254
65
255
66
export type AppBskyGraphDefsListPurpose =
256
67
| "app.bsky.graph.defs#modlist"
···
1144
955
| "createdAt"
1145
956
| "expiresAt";
1146
957
958
+
export interface NetworkSlicesSliceSyncUserCollectionsInput {
959
+
slice: string;
960
+
timeoutSeconds?: number;
961
+
}
962
+
963
+
export interface NetworkSlicesSliceSyncUserCollectionsOutput {
964
+
reposProcessed: number;
965
+
recordsSynced: number;
966
+
timedOut: boolean;
967
+
}
968
+
1147
969
export interface NetworkSlicesSliceDefsSliceView {
1148
970
uri: string;
1149
971
cid: string;
···
1169
991
count: number;
1170
992
}
1171
993
994
+
export interface NetworkSlicesSliceGetSparklinesInput {
995
+
slices: string[];
996
+
interval?: string;
997
+
duration?: string;
998
+
}
999
+
1000
+
export interface NetworkSlicesSliceGetSparklinesOutput {
1001
+
sparklines: NetworkSlicesSliceGetSparklines["SparklineEntry"][];
1002
+
}
1003
+
1004
+
export interface NetworkSlicesSliceGetSparklinesSparklineEntry {
1005
+
/** AT-URI of the slice */
1006
+
sliceUri: string;
1007
+
/** Array of sparkline data points */
1008
+
points: NetworkSlicesSliceDefs["SparklinePoint"][];
1009
+
}
1010
+
1011
+
export interface NetworkSlicesSliceGetJobLogsParams {
1012
+
jobId: string;
1013
+
limit?: number;
1014
+
}
1015
+
1016
+
export interface NetworkSlicesSliceGetJobLogsOutput {
1017
+
logs: NetworkSlicesSliceGetJobLogs["LogEntry"][];
1018
+
}
1019
+
1020
+
export interface NetworkSlicesSliceGetJobLogsLogEntry {
1021
+
/** Log entry ID */
1022
+
id: number;
1023
+
/** When the log entry was created */
1024
+
createdAt: string;
1025
+
/** Type of log entry */
1026
+
logType: string;
1027
+
/** UUID of related job if applicable */
1028
+
jobId?: string;
1029
+
/** DID of related user if applicable */
1030
+
userDid?: string;
1031
+
/** AT-URI of related slice if applicable */
1032
+
sliceUri?: string;
1033
+
/** Log level */
1034
+
level: string;
1035
+
/** Log message */
1036
+
message: string;
1037
+
/** Additional metadata associated with the log entry */
1038
+
metadata?: unknown;
1039
+
}
1040
+
1041
+
export interface NetworkSlicesSliceGetJetstreamStatusOutput {
1042
+
connected: boolean;
1043
+
}
1044
+
1172
1045
export interface NetworkSlicesSlice {
1173
1046
/** Name of the slice */
1174
1047
name: string;
···
1180
1053
1181
1054
export type NetworkSlicesSliceSortFields = "name" | "domain" | "createdAt";
1182
1055
1056
+
export interface NetworkSlicesSliceGetJobStatusParams {
1057
+
jobId: string;
1058
+
}
1059
+
1060
+
export type NetworkSlicesSliceGetJobStatusOutput =
1061
+
NetworkSlicesSliceGetJobStatus["JobStatus"];
1062
+
1063
+
export interface NetworkSlicesSliceGetJobStatusJobStatus {
1064
+
/** UUID of the job */
1065
+
jobId: string;
1066
+
/** Current status of the job */
1067
+
status: string;
1068
+
/** When the job was created */
1069
+
createdAt: string;
1070
+
/** When the job started executing */
1071
+
startedAt?: string;
1072
+
/** When the job completed */
1073
+
completedAt?: string;
1074
+
/** Job result if completed successfully */
1075
+
result?: NetworkSlicesSliceGetJobStatus["SyncJobResult"];
1076
+
/** Error message if job failed */
1077
+
error?: string;
1078
+
/** Number of times the job has been retried */
1079
+
retryCount: number;
1080
+
}
1081
+
1082
+
export interface NetworkSlicesSliceGetJobStatusSyncJobResult {
1083
+
/** Whether the sync job completed successfully */
1084
+
success: boolean;
1085
+
/** Total number of records synced */
1086
+
totalRecords: number;
1087
+
/** List of collection NSIDs that were synced */
1088
+
collectionsSynced: string[];
1089
+
/** Number of repositories processed */
1090
+
reposProcessed: number;
1091
+
/** Human-readable message about the job completion */
1092
+
message: string;
1093
+
}
1094
+
1095
+
export interface NetworkSlicesSliceGetActorsInput {
1096
+
slice: string;
1097
+
limit?: number;
1098
+
cursor?: string;
1099
+
where?: unknown;
1100
+
}
1101
+
1102
+
export interface NetworkSlicesSliceGetActorsOutput {
1103
+
actors: NetworkSlicesSliceGetActors["Actor"][];
1104
+
cursor?: string;
1105
+
}
1106
+
1107
+
export interface NetworkSlicesSliceGetActorsActor {
1108
+
/** Decentralized identifier of the actor */
1109
+
did: string;
1110
+
/** Human-readable handle of the actor */
1111
+
handle?: string;
1112
+
/** AT-URI of the slice this actor is indexed in */
1113
+
sliceUri: string;
1114
+
/** When this actor was indexed */
1115
+
indexedAt: string;
1116
+
}
1117
+
1118
+
export interface NetworkSlicesSliceDeleteOAuthClientInput {
1119
+
clientId: string;
1120
+
}
1121
+
1122
+
export interface NetworkSlicesSliceDeleteOAuthClientOutput {
1123
+
message: string;
1124
+
}
1125
+
1126
+
export interface NetworkSlicesSliceCreateOAuthClientInput {
1127
+
sliceUri: string;
1128
+
clientName: string;
1129
+
redirectUris: string[];
1130
+
grantTypes?: string[];
1131
+
responseTypes?: string[];
1132
+
scope?: string;
1133
+
clientUri?: string;
1134
+
logoUri?: string;
1135
+
tosUri?: string;
1136
+
policyUri?: string;
1137
+
}
1138
+
1139
+
export type NetworkSlicesSliceCreateOAuthClientOutput =
1140
+
NetworkSlicesSliceGetOAuthClients["OauthClientDetails"];
1141
+
1142
+
export interface NetworkSlicesSliceGetJetstreamLogsParams {
1143
+
slice?: string;
1144
+
limit?: number;
1145
+
}
1146
+
1147
+
export interface NetworkSlicesSliceGetJetstreamLogsOutput {
1148
+
logs: NetworkSlicesSliceGetJobLogs["LogEntry"][];
1149
+
}
1150
+
1151
+
export interface NetworkSlicesSliceGetSliceRecordsInput {
1152
+
slice: string;
1153
+
limit?: number;
1154
+
cursor?: string;
1155
+
where?: unknown;
1156
+
sortBy?: unknown;
1157
+
}
1158
+
1159
+
export interface NetworkSlicesSliceGetSliceRecordsOutput {
1160
+
records: NetworkSlicesSliceGetSliceRecords["IndexedRecord"][];
1161
+
cursor?: string;
1162
+
}
1163
+
1164
+
export interface NetworkSlicesSliceGetSliceRecordsIndexedRecord {
1165
+
/** AT-URI of the record */
1166
+
uri: string;
1167
+
/** Content identifier of the record */
1168
+
cid: string;
1169
+
/** DID of the record creator */
1170
+
did: string;
1171
+
/** NSID of the collection this record belongs to */
1172
+
collection: string;
1173
+
/** The record value/content */
1174
+
value: unknown;
1175
+
/** When this record was indexed */
1176
+
indexedAt: string;
1177
+
}
1178
+
1179
+
export interface NetworkSlicesSliceStartSyncInput {
1180
+
slice: string;
1181
+
collections?: string[];
1182
+
externalCollections?: string[];
1183
+
repos?: string[];
1184
+
limitPerRepo?: number;
1185
+
skipValidation?: boolean;
1186
+
}
1187
+
1188
+
export interface NetworkSlicesSliceStartSyncOutput {
1189
+
jobId: string;
1190
+
message: string;
1191
+
}
1192
+
1193
+
export interface NetworkSlicesSliceGetJobHistoryParams {
1194
+
userDid: string;
1195
+
sliceUri: string;
1196
+
limit?: number;
1197
+
}
1198
+
1199
+
export type NetworkSlicesSliceGetJobHistoryOutput =
1200
+
NetworkSlicesSliceGetJobStatus["JobStatus"][];
1201
+
1202
+
export interface NetworkSlicesSliceStatsParams {
1203
+
slice: string;
1204
+
}
1205
+
1206
+
export interface NetworkSlicesSliceStatsOutput {
1207
+
collections: string[];
1208
+
collectionStats: NetworkSlicesSliceStats["CollectionStats"][];
1209
+
totalLexicons: number;
1210
+
totalRecords: number;
1211
+
totalActors: number;
1212
+
}
1213
+
1214
+
export interface NetworkSlicesSliceStatsCollectionStats {
1215
+
/** Collection NSID */
1216
+
collection: string;
1217
+
/** Number of records in this collection */
1218
+
recordCount: number;
1219
+
/** Number of unique actors with records in this collection */
1220
+
uniqueActors: number;
1221
+
}
1222
+
1223
+
export interface NetworkSlicesSliceUpdateOAuthClientInput {
1224
+
clientId: string;
1225
+
clientName?: string;
1226
+
redirectUris?: string[];
1227
+
scope?: string;
1228
+
clientUri?: string;
1229
+
logoUri?: string;
1230
+
tosUri?: string;
1231
+
policyUri?: string;
1232
+
}
1233
+
1234
+
export type NetworkSlicesSliceUpdateOAuthClientOutput =
1235
+
NetworkSlicesSliceGetOAuthClients["OauthClientDetails"];
1236
+
1237
+
export interface NetworkSlicesSliceGetOAuthClientsParams {
1238
+
slice: string;
1239
+
}
1240
+
1241
+
export interface NetworkSlicesSliceGetOAuthClientsOutput {
1242
+
clients: NetworkSlicesSliceGetOAuthClients["OauthClientDetails"][];
1243
+
}
1244
+
1245
+
export interface NetworkSlicesSliceGetOAuthClientsOauthClientDetails {
1246
+
/** OAuth client ID */
1247
+
clientId: string;
1248
+
/** OAuth client secret (only returned on creation) */
1249
+
clientSecret?: string;
1250
+
/** Human-readable name of the OAuth client */
1251
+
clientName: string;
1252
+
/** Allowed redirect URIs for OAuth flow */
1253
+
redirectUris: string[];
1254
+
/** Allowed OAuth grant types */
1255
+
grantTypes: string[];
1256
+
/** Allowed OAuth response types */
1257
+
responseTypes: string[];
1258
+
/** OAuth scope */
1259
+
scope?: string;
1260
+
/** URI of the client application */
1261
+
clientUri?: string;
1262
+
/** URI of the client logo */
1263
+
logoUri?: string;
1264
+
/** URI of the terms of service */
1265
+
tosUri?: string;
1266
+
/** URI of the privacy policy */
1267
+
policyUri?: string;
1268
+
/** When the OAuth client was created */
1269
+
createdAt: string;
1270
+
/** DID of the user who created this client */
1271
+
createdByDid: string;
1272
+
}
1273
+
1183
1274
export interface NetworkSlicesLexicon {
1184
1275
/** Namespaced identifier for the lexicon */
1185
1276
nsid: string;
···
1438
1529
export interface NetworkSlicesSliceDefs {
1439
1530
readonly SliceView: NetworkSlicesSliceDefsSliceView;
1440
1531
readonly SparklinePoint: NetworkSlicesSliceDefsSparklinePoint;
1532
+
}
1533
+
1534
+
export interface NetworkSlicesSliceGetSparklines {
1535
+
readonly SparklineEntry: NetworkSlicesSliceGetSparklinesSparklineEntry;
1536
+
}
1537
+
1538
+
export interface NetworkSlicesSliceGetJobLogs {
1539
+
readonly LogEntry: NetworkSlicesSliceGetJobLogsLogEntry;
1540
+
}
1541
+
1542
+
export interface NetworkSlicesSliceGetJobStatus {
1543
+
readonly JobStatus: NetworkSlicesSliceGetJobStatusJobStatus;
1544
+
readonly SyncJobResult: NetworkSlicesSliceGetJobStatusSyncJobResult;
1545
+
}
1546
+
1547
+
export interface NetworkSlicesSliceGetActors {
1548
+
readonly Actor: NetworkSlicesSliceGetActorsActor;
1549
+
}
1550
+
1551
+
export interface NetworkSlicesSliceGetSliceRecords {
1552
+
readonly IndexedRecord: NetworkSlicesSliceGetSliceRecordsIndexedRecord;
1553
+
}
1554
+
1555
+
export interface NetworkSlicesSliceStats {
1556
+
readonly CollectionStats: NetworkSlicesSliceStatsCollectionStats;
1557
+
}
1558
+
1559
+
export interface NetworkSlicesSliceGetOAuthClients {
1560
+
readonly OauthClientDetails:
1561
+
NetworkSlicesSliceGetOAuthClientsOauthClientDetails;
1441
1562
}
1442
1563
1443
1564
export interface NetworkSlicesActorDefs {
···
2073
2194
return await this.client.deleteRecord("network.slices.slice", rkey);
2074
2195
}
2075
2196
2076
-
async stats(params: SliceStatsParams): Promise<SliceStatsOutput> {
2077
-
return await this.client.makeRequest<SliceStatsOutput>(
2078
-
"network.slices.slice.stats",
2079
-
"POST",
2080
-
params,
2081
-
);
2197
+
async syncUserCollections(
2198
+
input: NetworkSlicesSliceSyncUserCollectionsInput,
2199
+
): Promise<NetworkSlicesSliceSyncUserCollectionsOutput> {
2200
+
return await this.client.makeRequest<
2201
+
NetworkSlicesSliceSyncUserCollectionsOutput
2202
+
>("network.slices.slice.syncUserCollections", "POST", input);
2082
2203
}
2083
2204
2084
2205
async getSparklines(
2085
-
params: GetSparklinesParams,
2086
-
): Promise<GetSparklinesOutput> {
2087
-
return await this.client.makeRequest<GetSparklinesOutput>(
2206
+
input: NetworkSlicesSliceGetSparklinesInput,
2207
+
): Promise<NetworkSlicesSliceGetSparklinesOutput> {
2208
+
return await this.client.makeRequest<NetworkSlicesSliceGetSparklinesOutput>(
2088
2209
"network.slices.slice.getSparklines",
2089
2210
"POST",
2090
-
params,
2211
+
input,
2091
2212
);
2092
2213
}
2093
2214
2094
-
async getSliceRecords<T = Record<string, unknown>>(
2095
-
params: Omit<SliceLevelRecordsParams<T>, "slice">,
2096
-
): Promise<SliceRecordsOutput<T>> {
2097
-
// Combine where and orWhere into the expected backend format
2098
-
const whereClause: Record<string, unknown> = params?.where
2099
-
? { ...params.where }
2100
-
: {};
2101
-
if (params?.orWhere) {
2102
-
whereClause.$or = params.orWhere;
2103
-
}
2104
-
2105
-
const requestParams = {
2106
-
...params,
2107
-
where: Object.keys(whereClause).length > 0 ? whereClause : undefined,
2108
-
orWhere: undefined, // Remove orWhere as it's now in where.$or
2109
-
slice: this.client.sliceUri,
2110
-
};
2111
-
return await this.client.makeRequest<SliceRecordsOutput<T>>(
2112
-
"network.slices.slice.getSliceRecords",
2113
-
"POST",
2114
-
requestParams,
2115
-
);
2116
-
}
2117
-
2118
-
async getActors(params?: GetActorsParams): Promise<GetActorsResponse> {
2119
-
const requestParams = { ...params, slice: this.client.sliceUri };
2120
-
return await this.client.makeRequest<GetActorsResponse>(
2121
-
"network.slices.slice.getActors",
2122
-
"POST",
2123
-
requestParams,
2215
+
async getJobLogs(
2216
+
params?: NetworkSlicesSliceGetJobLogsParams,
2217
+
): Promise<NetworkSlicesSliceGetJobLogsOutput> {
2218
+
return await this.client.makeRequest<NetworkSlicesSliceGetJobLogsOutput>(
2219
+
"network.slices.slice.getJobLogs",
2220
+
"GET",
2221
+
params,
2124
2222
);
2125
2223
}
2126
2224
2127
-
async startSync(params: BulkSyncParams): Promise<SyncJobResponse> {
2128
-
const requestParams = { ...params, slice: this.client.sliceUri };
2129
-
return await this.client.makeRequest<SyncJobResponse>(
2130
-
"network.slices.slice.startSync",
2131
-
"POST",
2132
-
requestParams,
2133
-
);
2225
+
async getJetstreamStatus(): Promise<
2226
+
NetworkSlicesSliceGetJetstreamStatusOutput
2227
+
> {
2228
+
return await this.client.makeRequest<
2229
+
NetworkSlicesSliceGetJetstreamStatusOutput
2230
+
>("network.slices.slice.getJetstreamStatus", "GET", {});
2134
2231
}
2135
2232
2136
-
async getJobStatus(params: GetJobStatusParams): Promise<JobStatus> {
2137
-
return await this.client.makeRequest<JobStatus>(
2233
+
async getJobStatus(
2234
+
params?: NetworkSlicesSliceGetJobStatusParams,
2235
+
): Promise<NetworkSlicesSliceGetJobStatusOutput> {
2236
+
return await this.client.makeRequest<NetworkSlicesSliceGetJobStatusOutput>(
2138
2237
"network.slices.slice.getJobStatus",
2139
2238
"GET",
2140
2239
params,
2141
2240
);
2142
2241
}
2143
2242
2144
-
async getJobHistory(
2145
-
params: GetJobHistoryParams,
2146
-
): Promise<GetJobHistoryResponse> {
2147
-
return await this.client.makeRequest<GetJobHistoryResponse>(
2148
-
"network.slices.slice.getJobHistory",
2149
-
"GET",
2150
-
params,
2243
+
async getActors(
2244
+
input: NetworkSlicesSliceGetActorsInput,
2245
+
): Promise<NetworkSlicesSliceGetActorsOutput> {
2246
+
return await this.client.makeRequest<NetworkSlicesSliceGetActorsOutput>(
2247
+
"network.slices.slice.getActors",
2248
+
"POST",
2249
+
input,
2151
2250
);
2152
2251
}
2153
2252
2154
-
async getJobLogs(params: GetJobLogsParams): Promise<GetJobLogsResponse> {
2155
-
return await this.client.makeRequest<GetJobLogsResponse>(
2156
-
"network.slices.slice.getJobLogs",
2157
-
"GET",
2158
-
params,
2159
-
);
2253
+
async deleteOAuthClient(
2254
+
input: NetworkSlicesSliceDeleteOAuthClientInput,
2255
+
): Promise<NetworkSlicesSliceDeleteOAuthClientOutput> {
2256
+
return await this.client.makeRequest<
2257
+
NetworkSlicesSliceDeleteOAuthClientOutput
2258
+
>("network.slices.slice.deleteOAuthClient", "POST", input);
2160
2259
}
2161
2260
2162
-
async getJetstreamStatus(): Promise<JetstreamStatusResponse> {
2163
-
return await this.client.makeRequest<JetstreamStatusResponse>(
2164
-
"network.slices.slice.getJetstreamStatus",
2165
-
"GET",
2166
-
);
2261
+
async createOAuthClient(
2262
+
input: NetworkSlicesSliceCreateOAuthClientInput,
2263
+
): Promise<NetworkSlicesSliceCreateOAuthClientOutput> {
2264
+
return await this.client.makeRequest<
2265
+
NetworkSlicesSliceCreateOAuthClientOutput
2266
+
>("network.slices.slice.createOAuthClient", "POST", input);
2167
2267
}
2168
2268
2169
2269
async getJetstreamLogs(
2170
-
params: GetJetstreamLogsParams,
2171
-
): Promise<GetJetstreamLogsResponse> {
2172
-
const requestParams = { ...params, slice: this.client.sliceUri };
2173
-
return await this.client.makeRequest<GetJetstreamLogsResponse>(
2174
-
"network.slices.slice.getJetstreamLogs",
2175
-
"GET",
2176
-
requestParams,
2177
-
);
2270
+
params?: NetworkSlicesSliceGetJetstreamLogsParams,
2271
+
): Promise<NetworkSlicesSliceGetJetstreamLogsOutput> {
2272
+
return await this.client.makeRequest<
2273
+
NetworkSlicesSliceGetJetstreamLogsOutput
2274
+
>("network.slices.slice.getJetstreamLogs", "GET", params);
2275
+
}
2276
+
2277
+
async getSliceRecords(
2278
+
input: NetworkSlicesSliceGetSliceRecordsInput,
2279
+
): Promise<NetworkSlicesSliceGetSliceRecordsOutput> {
2280
+
return await this.client.makeRequest<
2281
+
NetworkSlicesSliceGetSliceRecordsOutput
2282
+
>("network.slices.slice.getSliceRecords", "POST", input);
2178
2283
}
2179
2284
2180
-
async syncUserCollections(
2181
-
params?: SyncUserCollectionsRequest,
2182
-
): Promise<SyncUserCollectionsResult> {
2183
-
const requestParams = { slice: this.client.sliceUri, ...params };
2184
-
return await this.client.makeRequest<SyncUserCollectionsResult>(
2185
-
"network.slices.slice.syncUserCollections",
2285
+
async startSync(
2286
+
input: NetworkSlicesSliceStartSyncInput,
2287
+
): Promise<NetworkSlicesSliceStartSyncOutput> {
2288
+
return await this.client.makeRequest<NetworkSlicesSliceStartSyncOutput>(
2289
+
"network.slices.slice.startSync",
2186
2290
"POST",
2187
-
requestParams,
2291
+
input,
2188
2292
);
2189
2293
}
2190
2294
2191
-
async createOAuthClient(
2192
-
params: CreateOAuthClientRequest,
2193
-
): Promise<OAuthClientDetails | OAuthOperationError> {
2194
-
const requestParams = { ...params, sliceUri: this.client.sliceUri };
2195
-
return await this.client.makeRequest<
2196
-
OAuthClientDetails | OAuthOperationError
2197
-
>("network.slices.slice.createOAuthClient", "POST", requestParams);
2295
+
async getJobHistory(
2296
+
params?: NetworkSlicesSliceGetJobHistoryParams,
2297
+
): Promise<NetworkSlicesSliceGetJobHistoryOutput> {
2298
+
return await this.client.makeRequest<NetworkSlicesSliceGetJobHistoryOutput>(
2299
+
"network.slices.slice.getJobHistory",
2300
+
"GET",
2301
+
params,
2302
+
);
2198
2303
}
2199
2304
2200
-
async getOAuthClients(): Promise<ListOAuthClientsResponse> {
2201
-
const requestParams = { slice: this.client.sliceUri };
2202
-
return await this.client.makeRequest<ListOAuthClientsResponse>(
2203
-
"network.slices.slice.getOAuthClients",
2305
+
async stats(
2306
+
params?: NetworkSlicesSliceStatsParams,
2307
+
): Promise<NetworkSlicesSliceStatsOutput> {
2308
+
return await this.client.makeRequest<NetworkSlicesSliceStatsOutput>(
2309
+
"network.slices.slice.stats",
2204
2310
"GET",
2205
-
requestParams,
2311
+
params,
2206
2312
);
2207
2313
}
2208
2314
2209
2315
async updateOAuthClient(
2210
-
params: UpdateOAuthClientRequest,
2211
-
): Promise<OAuthClientDetails | OAuthOperationError> {
2212
-
const requestParams = { ...params, sliceUri: this.client.sliceUri };
2316
+
input: NetworkSlicesSliceUpdateOAuthClientInput,
2317
+
): Promise<NetworkSlicesSliceUpdateOAuthClientOutput> {
2213
2318
return await this.client.makeRequest<
2214
-
OAuthClientDetails | OAuthOperationError
2215
-
>("network.slices.slice.updateOAuthClient", "POST", requestParams);
2319
+
NetworkSlicesSliceUpdateOAuthClientOutput
2320
+
>("network.slices.slice.updateOAuthClient", "POST", input);
2216
2321
}
2217
2322
2218
-
async deleteOAuthClient(
2219
-
clientId: string,
2220
-
): Promise<DeleteOAuthClientResponse> {
2221
-
return await this.client.makeRequest<DeleteOAuthClientResponse>(
2222
-
"network.slices.slice.deleteOAuthClient",
2223
-
"POST",
2224
-
{ clientId },
2225
-
);
2323
+
async getOAuthClients(
2324
+
params?: NetworkSlicesSliceGetOAuthClientsParams,
2325
+
): Promise<NetworkSlicesSliceGetOAuthClientsOutput> {
2326
+
return await this.client.makeRequest<
2327
+
NetworkSlicesSliceGetOAuthClientsOutput
2328
+
>("network.slices.slice.getOAuthClients", "GET", params);
2226
2329
}
2227
2330
}
2228
2331
+13
-11
packages/client/src/mod.ts
+13
-11
packages/client/src/mod.ts
···
26
26
}
27
27
28
28
export interface CountRecordsResponse {
29
-
success: boolean;
30
29
count: number;
31
-
message?: string;
32
30
}
33
31
34
32
export interface GetRecordParams {
···
105
103
}
106
104
107
105
export interface SliceRecordsOutput<T = Record<string, unknown>> {
108
-
success: boolean;
109
106
records: IndexedRecord<T>[];
110
107
cursor?: string;
111
-
message?: string;
112
108
}
113
109
114
110
// Blob upload interfaces
···
199
195
200
196
if (httpMethod === "GET" && params) {
201
197
const searchParams = new URLSearchParams();
202
-
Object.entries(params).forEach(([key, value]) => {
198
+
Object.entries(params as Record<string, unknown>).forEach(([key, value]) => {
203
199
if (value !== undefined && value !== null) {
204
200
searchParams.append(key, String(value));
205
201
}
···
251
247
252
248
try {
253
249
const errorBody = await response.json();
254
-
if (errorBody?.message) {
255
-
errorMessage += ` - ${errorBody.message}`;
250
+
// XRPC-style error format: { error: "ErrorName", message: "details" }
251
+
if (errorBody?.error && errorBody?.message) {
252
+
errorMessage = `${errorBody.error}: ${errorBody.message}`;
253
+
} else if (errorBody?.message) {
254
+
errorMessage = errorBody.message;
256
255
} else if (errorBody?.error) {
257
-
errorMessage += ` - ${errorBody.error}`;
256
+
errorMessage = errorBody.error;
258
257
}
259
258
} catch {
260
259
// If we can't parse the response body, just use the status message
···
355
354
let errorMessage = `Blob upload failed: ${response.status} ${response.statusText}`;
356
355
try {
357
356
const errorBody = await response.json();
358
-
if (errorBody?.message) {
359
-
errorMessage += ` - ${errorBody.message}`;
357
+
// XRPC-style error format: { error: "ErrorName", message: "details" }
358
+
if (errorBody?.error && errorBody?.message) {
359
+
errorMessage = `${errorBody.error}: ${errorBody.message}`;
360
+
} else if (errorBody?.message) {
361
+
errorMessage = errorBody.message;
360
362
} else if (errorBody?.error) {
361
-
errorMessage += ` - ${errorBody.error}`;
363
+
errorMessage = errorBody.error;
362
364
}
363
365
} catch {
364
366
// If we can't parse the response body, just use the status message
+181
-206
packages/codegen/src/client.ts
+181
-206
packages/codegen/src/client.ts
···
4
4
import { nsidToPascalCase, capitalizeFirst } from "./mod.ts";
5
5
6
6
interface NestedStructure {
7
-
[key: string]: NestedStructure | string | undefined;
7
+
[key: string]: NestedStructure | string | QueryProcedureInfo[] | undefined;
8
8
_recordType?: string;
9
9
_collectionPath?: string;
10
+
_queryProcedures?: QueryProcedureInfo[];
11
+
}
12
+
13
+
interface QueryProcedureInfo {
14
+
nsid: string;
15
+
type: "query" | "procedure";
16
+
methodName: string;
17
+
parametersType?: string;
18
+
inputType?: string;
19
+
outputType?: string;
10
20
}
11
21
12
22
interface PropertyInfo {
···
22
32
23
33
export function generateClient(
24
34
sourceFile: SourceFile,
25
-
lexicons: Lexicon[],
26
-
excludeSlicesClient: boolean
35
+
lexicons: Lexicon[]
27
36
): void {
28
37
// Create nested structure from lexicons
29
38
const nestedStructure: NestedStructure = {};
30
39
31
40
for (const lexicon of lexicons) {
32
41
if (lexicon.definitions && typeof lexicon.definitions === "object") {
33
-
for (const [, defValue] of Object.entries(lexicon.definitions)) {
34
-
if (defValue.type === "record" && defValue.record) {
35
-
const parts = lexicon.id.split(".");
36
-
let current = nestedStructure;
42
+
// Check if this lexicon has any records, queries, or procedures
43
+
const hasRecordsOrEndpoints = Object.values(lexicon.definitions).some(
44
+
(defValue) =>
45
+
(defValue.type === "record" && defValue.record) ||
46
+
defValue.type === "query" ||
47
+
defValue.type === "procedure"
48
+
);
49
+
50
+
// Only build nested structure for lexicons that have records
51
+
if (hasRecordsOrEndpoints) {
52
+
for (const [_, defValue] of Object.entries(lexicon.definitions)) {
53
+
if (defValue.type === "record" && defValue.record) {
54
+
const parts = lexicon.id.split(".");
55
+
let current = nestedStructure;
37
56
38
-
// Build nested structure
39
-
for (const part of parts) {
40
-
if (!current[part]) {
41
-
current[part] = {};
57
+
// Build nested structure
58
+
for (const part of parts) {
59
+
if (!current[part]) {
60
+
current[part] = {};
61
+
}
62
+
current = current[part] as NestedStructure;
42
63
}
43
-
current = current[part] as NestedStructure;
64
+
65
+
// Add the record interface name and store collection path
66
+
current._recordType = nsidToPascalCase(lexicon.id);
67
+
current._collectionPath = lexicon.id;
44
68
}
69
+
}
70
+
}
71
+
}
72
+
}
45
73
46
-
// Add the record interface name and store collection path
47
-
current._recordType = nsidToPascalCase(lexicon.id);
48
-
current._collectionPath = lexicon.id;
74
+
// Add query/procedure methods to the appropriate parent clients
75
+
function addQueryProcedureMethods(
76
+
obj: NestedStructure,
77
+
lexicons: Lexicon[]
78
+
): void {
79
+
// Group query/procedure endpoints by their parent collection path
80
+
const endpointsByParent = new Map<string, QueryProcedureInfo[]>();
81
+
82
+
for (const lexicon of lexicons) {
83
+
if (lexicon.definitions && typeof lexicon.definitions === "object") {
84
+
for (const [_, defValue] of Object.entries(lexicon.definitions)) {
85
+
if (defValue.type === "query" || defValue.type === "procedure") {
86
+
const parts = lexicon.id.split(".");
87
+
const methodName = parts[parts.length - 1];
88
+
89
+
// Find the parent collection by removing the method name
90
+
// e.g., "network.slices.slice.getJobStatus" -> "network.slices.slice"
91
+
const parentPath = parts.slice(0, -1).join(".");
92
+
93
+
const queryProcedureInfo: QueryProcedureInfo = {
94
+
nsid: lexicon.id,
95
+
type: defValue.type as "query" | "procedure",
96
+
methodName,
97
+
parametersType:
98
+
defValue.parameters?.properties &&
99
+
Object.keys(defValue.parameters.properties).length > 0
100
+
? `${nsidToPascalCase(lexicon.id)}Params`
101
+
: undefined,
102
+
inputType: defValue.input
103
+
? `${nsidToPascalCase(lexicon.id)}Input`
104
+
: undefined,
105
+
outputType: defValue.output
106
+
? `${nsidToPascalCase(lexicon.id)}Output`
107
+
: undefined,
108
+
};
109
+
110
+
if (!endpointsByParent.has(parentPath)) {
111
+
endpointsByParent.set(parentPath, []);
112
+
}
113
+
endpointsByParent.get(parentPath)!.push(queryProcedureInfo);
114
+
}
49
115
}
50
116
}
51
117
}
118
+
119
+
// Add endpoints to their respective parent clients
120
+
function addEndpointsToNode(
121
+
current: NestedStructure,
122
+
path: string[],
123
+
fullPath: string
124
+
): void {
125
+
if (path.length === 0) {
126
+
// We've reached the target node, add the endpoints if this has a record type
127
+
const endpoints = endpointsByParent.get(fullPath);
128
+
if (endpoints && current._recordType) {
129
+
if (!current._queryProcedures) {
130
+
current._queryProcedures = [];
131
+
}
132
+
current._queryProcedures.push(...endpoints);
133
+
}
134
+
return;
135
+
}
136
+
137
+
const [head, ...tail] = path;
138
+
if (current[head]) {
139
+
addEndpointsToNode(current[head] as NestedStructure, tail, fullPath);
140
+
}
141
+
}
142
+
143
+
// Add endpoints to their parent nodes
144
+
for (const parentPath of endpointsByParent.keys()) {
145
+
const pathParts = parentPath.split(".");
146
+
addEndpointsToNode(obj, pathParts, parentPath);
147
+
}
52
148
}
149
+
150
+
// Add query/procedure methods before generating classes
151
+
addQueryProcedureMethods(nestedStructure, lexicons);
53
152
54
153
// Generate nested class structure
55
154
function generateNestedClass(
···
128
227
});
129
228
} else if (key === "_collectionPath") {
130
229
collectionPath = value as string;
230
+
} else if (key === "_queryProcedures") {
231
+
// Add query and procedure methods
232
+
const queryProcedures = value as QueryProcedureInfo[];
233
+
for (const qp of queryProcedures) {
234
+
if (qp.type === "query") {
235
+
// Generate query method (GET)
236
+
const parameters = [];
237
+
if (qp.parametersType) {
238
+
parameters.push({
239
+
name: "params",
240
+
type: qp.parametersType,
241
+
hasQuestionToken: true,
242
+
});
243
+
}
244
+
methods.push({
245
+
name: qp.methodName,
246
+
parameters,
247
+
returnType: `Promise<${qp.outputType || "void"}>`,
248
+
});
249
+
} else if (qp.type === "procedure") {
250
+
// Generate procedure method (POST)
251
+
const parameters = [];
252
+
if (qp.inputType) {
253
+
parameters.push({
254
+
name: "input",
255
+
type: qp.inputType,
256
+
});
257
+
} else if (qp.parametersType) {
258
+
parameters.push({
259
+
name: "params",
260
+
type: qp.parametersType,
261
+
});
262
+
}
263
+
methods.push({
264
+
name: qp.methodName,
265
+
parameters,
266
+
returnType: `Promise<${qp.outputType || "void"}>`,
267
+
});
268
+
}
269
+
}
131
270
} else if (typeof value === "object" && Object.keys(value).length > 0) {
132
271
// Add nested property with PascalCase class name
133
272
const nestedClassName = `${capitalizeFirst(key)}${className}`;
···
246
385
methodDecl.addStatements([
247
386
`return await ${clientRef}.countRecords('${collectionPath}', params);`,
248
387
]);
388
+
} else {
389
+
// Handle query and procedure methods
390
+
const queryProcedures = obj._queryProcedures || [];
391
+
const matchingQP = queryProcedures.find(
392
+
(qp) => qp.methodName === method.name
393
+
);
394
+
395
+
if (matchingQP) {
396
+
if (matchingQP.type === "query") {
397
+
// Query methods use GET with query parameters
398
+
const paramArg = method.parameters.length > 0 ? "params" : "{}";
399
+
methodDecl.addStatements([
400
+
`return await ${clientRef}.makeRequest<${
401
+
matchingQP.outputType || "void"
402
+
}>('${matchingQP.nsid}', 'GET', ${paramArg});`,
403
+
]);
404
+
} else if (matchingQP.type === "procedure") {
405
+
// Procedure methods use POST with body
406
+
const paramArg =
407
+
method.parameters.length > 0 ? method.parameters[0].name : "{}";
408
+
methodDecl.addStatements([
409
+
`return await ${clientRef}.makeRequest<${
410
+
matchingQP.outputType || "void"
411
+
}>('${matchingQP.nsid}', 'POST', ${paramArg});`,
412
+
]);
413
+
}
414
+
}
249
415
}
250
-
}
251
-
252
-
// Add network.slices.slice specific methods when not excluding slices client
253
-
if (
254
-
!excludeSlicesClient &&
255
-
currentPath.length === 3 &&
256
-
currentPath[0] === "network" &&
257
-
currentPath[1] === "slices" &&
258
-
currentPath[2] === "slice"
259
-
) {
260
-
classDeclaration.addMethod({
261
-
name: "stats",
262
-
parameters: [{ name: "params", type: "SliceStatsParams" }],
263
-
returnType: "Promise<SliceStatsOutput>",
264
-
isAsync: true,
265
-
statements: [
266
-
`return await this.client.makeRequest<SliceStatsOutput>('network.slices.slice.stats', 'POST', params);`,
267
-
],
268
-
});
269
-
270
-
classDeclaration.addMethod({
271
-
name: "getSparklines",
272
-
parameters: [{ name: "params", type: "GetSparklinesParams" }],
273
-
returnType: "Promise<GetSparklinesOutput>",
274
-
isAsync: true,
275
-
statements: [
276
-
`return await this.client.makeRequest<GetSparklinesOutput>('network.slices.slice.getSparklines', 'POST', params);`,
277
-
],
278
-
});
279
-
280
-
classDeclaration.addMethod({
281
-
name: "getSliceRecords",
282
-
typeParameters: [{ name: "T", default: "Record<string, unknown>" }],
283
-
parameters: [
284
-
{
285
-
name: "params",
286
-
type: "Omit<SliceLevelRecordsParams<T>, 'slice'>",
287
-
},
288
-
],
289
-
returnType: "Promise<SliceRecordsOutput<T>>",
290
-
isAsync: true,
291
-
statements: [
292
-
`// Combine where and orWhere into the expected backend format`,
293
-
`const whereClause: Record<string, unknown> = params?.where ? { ...params.where } : {};`,
294
-
`if (params?.orWhere) {`,
295
-
` whereClause.$or = params.orWhere;`,
296
-
`}`,
297
-
`const requestParams = {`,
298
-
` ...params,`,
299
-
` where: Object.keys(whereClause).length > 0 ? whereClause : undefined,`,
300
-
` orWhere: undefined, // Remove orWhere as it's now in where.$or`,
301
-
` slice: this.client.sliceUri`,
302
-
`};`,
303
-
`return await this.client.makeRequest<SliceRecordsOutput<T>>('network.slices.slice.getSliceRecords', 'POST', requestParams);`,
304
-
],
305
-
});
306
-
307
-
classDeclaration.addMethod({
308
-
name: "getActors",
309
-
parameters: [
310
-
{ name: "params", type: "GetActorsParams", hasQuestionToken: true },
311
-
],
312
-
returnType: "Promise<GetActorsResponse>",
313
-
isAsync: true,
314
-
statements: [
315
-
`const requestParams = { ...params, slice: this.client.sliceUri };`,
316
-
`return await this.client.makeRequest<GetActorsResponse>('network.slices.slice.getActors', 'POST', requestParams);`,
317
-
],
318
-
});
319
-
320
-
// Add sync methods
321
-
classDeclaration.addMethod({
322
-
name: "startSync",
323
-
parameters: [{ name: "params", type: "BulkSyncParams" }],
324
-
returnType: "Promise<SyncJobResponse>",
325
-
isAsync: true,
326
-
statements: [
327
-
`const requestParams = { ...params, slice: this.client.sliceUri };`,
328
-
`return await this.client.makeRequest<SyncJobResponse>('network.slices.slice.startSync', 'POST', requestParams);`,
329
-
],
330
-
});
331
-
332
-
classDeclaration.addMethod({
333
-
name: "getJobStatus",
334
-
parameters: [{ name: "params", type: "GetJobStatusParams" }],
335
-
returnType: "Promise<JobStatus>",
336
-
isAsync: true,
337
-
statements: [
338
-
`return await this.client.makeRequest<JobStatus>('network.slices.slice.getJobStatus', 'GET', params);`,
339
-
],
340
-
});
341
-
342
-
classDeclaration.addMethod({
343
-
name: "getJobHistory",
344
-
parameters: [{ name: "params", type: "GetJobHistoryParams" }],
345
-
returnType: "Promise<GetJobHistoryResponse>",
346
-
isAsync: true,
347
-
statements: [
348
-
`return await this.client.makeRequest<GetJobHistoryResponse>('network.slices.slice.getJobHistory', 'GET', params);`,
349
-
],
350
-
});
351
-
352
-
classDeclaration.addMethod({
353
-
name: "getJobLogs",
354
-
parameters: [{ name: "params", type: "GetJobLogsParams" }],
355
-
returnType: "Promise<GetJobLogsResponse>",
356
-
isAsync: true,
357
-
statements: [
358
-
`return await this.client.makeRequest<GetJobLogsResponse>('network.slices.slice.getJobLogs', 'GET', params);`,
359
-
],
360
-
});
361
-
362
-
classDeclaration.addMethod({
363
-
name: "getJetstreamStatus",
364
-
returnType: "Promise<JetstreamStatusResponse>",
365
-
isAsync: true,
366
-
statements: [
367
-
`return await this.client.makeRequest<JetstreamStatusResponse>('network.slices.slice.getJetstreamStatus', 'GET');`,
368
-
],
369
-
});
370
-
371
-
classDeclaration.addMethod({
372
-
name: "getJetstreamLogs",
373
-
parameters: [{ name: "params", type: "GetJetstreamLogsParams" }],
374
-
returnType: "Promise<GetJetstreamLogsResponse>",
375
-
isAsync: true,
376
-
statements: [
377
-
`const requestParams = { ...params, slice: this.client.sliceUri };`,
378
-
`return await this.client.makeRequest<GetJetstreamLogsResponse>('network.slices.slice.getJetstreamLogs', 'GET', requestParams);`,
379
-
],
380
-
});
381
-
382
-
classDeclaration.addMethod({
383
-
name: "syncUserCollections",
384
-
parameters: [
385
-
{
386
-
name: "params",
387
-
type: "SyncUserCollectionsRequest",
388
-
hasQuestionToken: true,
389
-
},
390
-
],
391
-
returnType: "Promise<SyncUserCollectionsResult>",
392
-
isAsync: true,
393
-
statements: [
394
-
`const requestParams = { slice: this.client.sliceUri, ...params };`,
395
-
`return await this.client.makeRequest<SyncUserCollectionsResult>('network.slices.slice.syncUserCollections', 'POST', requestParams);`,
396
-
],
397
-
});
398
-
399
-
// Add OAuth client management methods
400
-
classDeclaration.addMethod({
401
-
name: "createOAuthClient",
402
-
parameters: [{ name: "params", type: "CreateOAuthClientRequest" }],
403
-
returnType: "Promise<OAuthClientDetails | OAuthOperationError>",
404
-
isAsync: true,
405
-
statements: [
406
-
`const requestParams = { ...params, sliceUri: this.client.sliceUri };`,
407
-
`return await this.client.makeRequest<OAuthClientDetails | OAuthOperationError>('network.slices.slice.createOAuthClient', 'POST', requestParams);`,
408
-
],
409
-
});
410
-
411
-
classDeclaration.addMethod({
412
-
name: "getOAuthClients",
413
-
returnType: "Promise<ListOAuthClientsResponse>",
414
-
isAsync: true,
415
-
statements: [
416
-
`const requestParams = { slice: this.client.sliceUri };`,
417
-
`return await this.client.makeRequest<ListOAuthClientsResponse>('network.slices.slice.getOAuthClients', 'GET', requestParams);`,
418
-
],
419
-
});
420
-
421
-
classDeclaration.addMethod({
422
-
name: "updateOAuthClient",
423
-
parameters: [{ name: "params", type: "UpdateOAuthClientRequest" }],
424
-
returnType: "Promise<OAuthClientDetails | OAuthOperationError>",
425
-
isAsync: true,
426
-
statements: [
427
-
`const requestParams = { ...params, sliceUri: this.client.sliceUri };`,
428
-
`return await this.client.makeRequest<OAuthClientDetails | OAuthOperationError>('network.slices.slice.updateOAuthClient', 'POST', requestParams);`,
429
-
],
430
-
});
431
-
432
-
classDeclaration.addMethod({
433
-
name: "deleteOAuthClient",
434
-
parameters: [{ name: "clientId", type: "string" }],
435
-
returnType: "Promise<DeleteOAuthClientResponse>",
436
-
isAsync: true,
437
-
statements: [
438
-
`return await this.client.makeRequest<DeleteOAuthClientResponse>('network.slices.slice.deleteOAuthClient', 'POST', { clientId });`,
439
-
],
440
-
});
441
416
}
442
417
}
443
418
}
+110
-292
packages/codegen/src/interfaces.ts
+110
-292
packages/codegen/src/interfaces.ts
···
547
547
548
548
// Base interfaces are imported from @slices/client, only add network.slices specific interfaces
549
549
function addBaseInterfaces(
550
+
_sourceFile: SourceFile
551
+
): void {
552
+
// All interfaces are now generated from lexicons
553
+
// This function is kept for future extensibility if needed
554
+
}
555
+
556
+
// Generate interfaces for query and procedure parameters/input/output
557
+
function generateQueryProcedureInterfaces(
550
558
sourceFile: SourceFile,
551
-
excludeSlicesClient: boolean
559
+
lexicon: Lexicon,
560
+
defValue: LexiconDefinition,
561
+
lexicons: Lexicon[],
562
+
type: "query" | "procedure"
552
563
): void {
553
-
// Only generate network.slices specific interfaces when not excluding slices client
554
-
if (!excludeSlicesClient) {
555
-
// Codegen XRPC interfaces
564
+
const baseName = nsidToPascalCase(lexicon.id);
556
565
557
-
// Sync interfaces
558
-
sourceFile.addInterface({
559
-
name: "BulkSyncParams",
560
-
isExported: true,
561
-
properties: [
562
-
{ name: "collections", type: "string[]", hasQuestionToken: true },
563
-
{
564
-
name: "externalCollections",
565
-
type: "string[]",
566
-
hasQuestionToken: true,
567
-
},
568
-
{ name: "repos", type: "string[]", hasQuestionToken: true },
569
-
{ name: "limitPerRepo", type: "number", hasQuestionToken: true },
570
-
],
571
-
});
572
-
573
-
sourceFile.addInterface({
574
-
name: "BulkSyncOutput",
575
-
isExported: true,
576
-
properties: [
577
-
{ name: "success", type: "boolean" },
578
-
{ name: "totalRecords", type: "number" },
579
-
{ name: "collectionsSynced", type: "string[]" },
580
-
{ name: "reposProcessed", type: "number" },
581
-
{ name: "message", type: "string" },
582
-
],
583
-
});
584
-
585
-
// Job queue interfaces
586
-
sourceFile.addInterface({
587
-
name: "SyncJobResponse",
588
-
isExported: true,
589
-
properties: [
590
-
{ name: "success", type: "boolean" },
591
-
{ name: "jobId", type: "string", hasQuestionToken: true },
592
-
{ name: "message", type: "string" },
593
-
],
594
-
});
595
-
596
-
sourceFile.addInterface({
597
-
name: "SyncJobResult",
598
-
isExported: true,
599
-
properties: [
600
-
{ name: "success", type: "boolean" },
601
-
{ name: "totalRecords", type: "number" },
602
-
{ name: "collectionsSynced", type: "string[]" },
603
-
{ name: "reposProcessed", type: "number" },
604
-
{ name: "message", type: "string" },
605
-
],
606
-
});
607
-
608
-
sourceFile.addInterface({
609
-
name: "JobStatus",
610
-
isExported: true,
611
-
properties: [
612
-
{ name: "jobId", type: "string" },
613
-
{ name: "status", type: "string" },
614
-
{ name: "createdAt", type: "string" },
615
-
{ name: "startedAt", type: "string", hasQuestionToken: true },
616
-
{ name: "completedAt", type: "string", hasQuestionToken: true },
617
-
{ name: "result", type: "SyncJobResult", hasQuestionToken: true },
618
-
{ name: "error", type: "string", hasQuestionToken: true },
619
-
{ name: "retryCount", type: "number" },
620
-
],
621
-
});
622
-
623
-
sourceFile.addInterface({
624
-
name: "GetJobStatusParams",
625
-
isExported: true,
626
-
properties: [{ name: "jobId", type: "string" }],
627
-
});
628
-
629
-
sourceFile.addInterface({
630
-
name: "GetJobHistoryParams",
631
-
isExported: true,
632
-
properties: [
633
-
{ name: "userDid", type: "string" },
634
-
{ name: "sliceUri", type: "string" },
635
-
{ name: "limit", type: "number", hasQuestionToken: true },
636
-
],
637
-
});
638
-
639
-
sourceFile.addTypeAlias({
640
-
name: "GetJobHistoryResponse",
641
-
isExported: true,
642
-
type: "JobStatus[]",
643
-
});
644
-
645
-
sourceFile.addInterface({
646
-
name: "GetJobLogsParams",
647
-
isExported: true,
648
-
properties: [
649
-
{ name: "jobId", type: "string" },
650
-
{ name: "limit", type: "number", hasQuestionToken: true },
651
-
],
652
-
});
653
-
654
-
sourceFile.addInterface({
655
-
name: "GetJobLogsResponse",
656
-
isExported: true,
657
-
properties: [{ name: "logs", type: "LogEntry[]" }],
658
-
});
659
-
660
-
sourceFile.addInterface({
661
-
name: "GetJetstreamLogsParams",
662
-
isExported: true,
663
-
properties: [{ name: "limit", type: "number", hasQuestionToken: true }],
664
-
});
665
-
666
-
sourceFile.addInterface({
667
-
name: "GetJetstreamLogsResponse",
668
-
isExported: true,
669
-
properties: [{ name: "logs", type: "LogEntry[]" }],
670
-
});
671
-
672
-
sourceFile.addInterface({
673
-
name: "LogEntry",
674
-
isExported: true,
675
-
properties: [
676
-
{ name: "id", type: "number" },
677
-
{ name: "createdAt", type: "string" },
678
-
{ name: "logType", type: "string" },
679
-
{ name: "jobId", type: "string", hasQuestionToken: true },
680
-
{ name: "userDid", type: "string", hasQuestionToken: true },
681
-
{ name: "sliceUri", type: "string", hasQuestionToken: true },
682
-
{ name: "level", type: "string" },
683
-
{ name: "message", type: "string" },
684
-
{
685
-
name: "metadata",
686
-
type: "Record<string, unknown>",
687
-
hasQuestionToken: true,
688
-
},
689
-
],
690
-
});
691
-
692
-
// Sync user collections interfaces
693
-
sourceFile.addInterface({
694
-
name: "SyncUserCollectionsRequest",
695
-
isExported: true,
696
-
properties: [
697
-
{ name: "slice", type: "string" },
698
-
{ name: "timeoutSeconds", type: "number", hasQuestionToken: true },
699
-
],
700
-
});
701
-
702
-
sourceFile.addInterface({
703
-
name: "SyncUserCollectionsResult",
704
-
isExported: true,
705
-
properties: [
706
-
{ name: "success", type: "boolean" },
707
-
{ name: "reposProcessed", type: "number" },
708
-
{ name: "recordsSynced", type: "number" },
709
-
{ name: "timedOut", type: "boolean" },
710
-
{ name: "message", type: "string" },
711
-
],
712
-
});
713
-
714
-
sourceFile.addInterface({
715
-
name: "JetstreamStatusResponse",
716
-
isExported: true,
717
-
properties: [
718
-
{ name: "connected", type: "boolean" },
719
-
{ name: "status", type: "string" },
720
-
{ name: "error", type: "string", hasQuestionToken: true },
721
-
],
722
-
});
723
-
724
-
sourceFile.addInterface({
725
-
name: "CollectionStats",
726
-
isExported: true,
727
-
properties: [
728
-
{ name: "collection", type: "string" },
729
-
{ name: "recordCount", type: "number" },
730
-
{ name: "uniqueActors", type: "number" },
731
-
],
732
-
});
733
-
734
-
sourceFile.addInterface({
735
-
name: "SliceStatsParams",
736
-
isExported: true,
737
-
properties: [{ name: "slice", type: "string" }],
738
-
});
739
-
740
-
sourceFile.addInterface({
741
-
name: "SliceStatsOutput",
742
-
isExported: true,
743
-
properties: [
744
-
{ name: "success", type: "boolean" },
745
-
{ name: "collections", type: "string[]" },
746
-
{ name: "collectionStats", type: "CollectionStats[]" },
747
-
{ name: "totalLexicons", type: "number" },
748
-
{ name: "totalRecords", type: "number" },
749
-
{ name: "totalActors", type: "number" },
750
-
{ name: "message", type: "string", hasQuestionToken: true },
751
-
],
752
-
});
566
+
// Generate parameters interface if present and has properties
567
+
if (defValue.parameters?.properties && Object.keys(defValue.parameters.properties).length > 0) {
568
+
const interfaceName = `${baseName}Params`;
569
+
const properties = Object.entries(defValue.parameters.properties).map(
570
+
([propName, propDef]) => ({
571
+
name: propName,
572
+
type: convertLexiconTypeToTypeScript(
573
+
propDef,
574
+
lexicon.id,
575
+
propName,
576
+
lexicons
577
+
),
578
+
hasQuestionToken: !(defValue.parameters?.required || []).includes(propName),
579
+
})
580
+
);
753
581
754
582
sourceFile.addInterface({
755
-
name: "GetSparklinesParams",
583
+
name: interfaceName,
756
584
isExported: true,
757
-
properties: [
758
-
{ name: "slices", type: "string[]" },
759
-
{ name: "interval", type: "string", hasQuestionToken: true },
760
-
{ name: "duration", type: "string", hasQuestionToken: true },
761
-
],
585
+
properties,
762
586
});
587
+
}
763
588
764
-
sourceFile.addInterface({
765
-
name: "GetSparklinesOutput",
766
-
isExported: true,
767
-
properties: [
768
-
{ name: "success", type: "boolean" },
769
-
{
770
-
name: "sparklines",
771
-
type: `Record<string, NetworkSlicesSliceDefs["SparklinePoint"][]>`,
772
-
},
773
-
{ name: "message", type: "string", hasQuestionToken: true },
774
-
],
775
-
});
589
+
// Generate input interface for procedures
590
+
if (type === "procedure" && defValue.input?.schema) {
591
+
const interfaceName = `${baseName}Input`;
776
592
777
-
// OAuth client interfaces
778
-
sourceFile.addInterface({
779
-
name: "CreateOAuthClientRequest",
780
-
isExported: true,
781
-
properties: [
782
-
{ name: "clientName", type: "string" },
783
-
{ name: "redirectUris", type: "string[]" },
784
-
{ name: "grantTypes", type: "string[]", hasQuestionToken: true },
785
-
{ name: "responseTypes", type: "string[]", hasQuestionToken: true },
786
-
{ name: "scope", type: "string", hasQuestionToken: true },
787
-
{ name: "clientUri", type: "string", hasQuestionToken: true },
788
-
{ name: "logoUri", type: "string", hasQuestionToken: true },
789
-
{ name: "tosUri", type: "string", hasQuestionToken: true },
790
-
{ name: "policyUri", type: "string", hasQuestionToken: true },
791
-
],
792
-
});
593
+
if (defValue.input?.schema?.type === "object" && defValue.input.schema.properties) {
594
+
const properties = Object.entries(defValue.input.schema.properties).map(
595
+
([propName, propDef]) => ({
596
+
name: propName,
597
+
type: convertLexiconTypeToTypeScript(
598
+
propDef,
599
+
lexicon.id,
600
+
propName,
601
+
lexicons
602
+
),
603
+
hasQuestionToken: !((defValue.input?.schema?.required as string[]) || []).includes(propName),
604
+
})
605
+
);
793
606
794
-
sourceFile.addInterface({
795
-
name: "OAuthClientDetails",
796
-
isExported: true,
797
-
properties: [
798
-
{ name: "clientId", type: "string" },
799
-
{ name: "clientSecret", type: "string", hasQuestionToken: true },
800
-
{ name: "clientName", type: "string" },
801
-
{ name: "redirectUris", type: "string[]" },
802
-
{ name: "grantTypes", type: "string[]" },
803
-
{ name: "responseTypes", type: "string[]" },
804
-
{ name: "scope", type: "string", hasQuestionToken: true },
805
-
{ name: "clientUri", type: "string", hasQuestionToken: true },
806
-
{ name: "logoUri", type: "string", hasQuestionToken: true },
807
-
{ name: "tosUri", type: "string", hasQuestionToken: true },
808
-
{ name: "policyUri", type: "string", hasQuestionToken: true },
809
-
{ name: "createdAt", type: "string" },
810
-
{ name: "createdByDid", type: "string" },
811
-
],
812
-
});
607
+
sourceFile.addInterface({
608
+
name: interfaceName,
609
+
isExported: true,
610
+
properties,
611
+
});
612
+
}
613
+
}
813
614
814
-
sourceFile.addInterface({
815
-
name: "ListOAuthClientsResponse",
816
-
isExported: true,
817
-
properties: [{ name: "clients", type: "OAuthClientDetails[]" }],
818
-
});
615
+
// Generate output interface if present
616
+
if (defValue.output?.schema) {
617
+
const interfaceName = `${baseName}Output`;
819
618
820
-
sourceFile.addInterface({
821
-
name: "UpdateOAuthClientRequest",
822
-
isExported: true,
823
-
properties: [
824
-
{ name: "clientId", type: "string" },
825
-
{ name: "clientName", type: "string", hasQuestionToken: true },
826
-
{ name: "redirectUris", type: "string[]", hasQuestionToken: true },
827
-
{ name: "scope", type: "string", hasQuestionToken: true },
828
-
{ name: "clientUri", type: "string", hasQuestionToken: true },
829
-
{ name: "logoUri", type: "string", hasQuestionToken: true },
830
-
{ name: "tosUri", type: "string", hasQuestionToken: true },
831
-
{ name: "policyUri", type: "string", hasQuestionToken: true },
832
-
],
833
-
});
619
+
if (defValue.output?.schema?.type === "object" && defValue.output.schema.properties) {
620
+
const properties = Object.entries(defValue.output.schema.properties).map(
621
+
([propName, propDef]) => ({
622
+
name: propName,
623
+
type: convertLexiconTypeToTypeScript(
624
+
propDef,
625
+
lexicon.id,
626
+
propName,
627
+
lexicons
628
+
),
629
+
hasQuestionToken: !((defValue.output?.schema?.required as string[]) || []).includes(propName),
630
+
})
631
+
);
834
632
835
-
sourceFile.addInterface({
836
-
name: "DeleteOAuthClientResponse",
837
-
isExported: true,
838
-
properties: [
839
-
{ name: "success", type: "boolean" },
840
-
{ name: "message", type: "string" },
841
-
],
842
-
});
633
+
sourceFile.addInterface({
634
+
name: interfaceName,
635
+
isExported: true,
636
+
properties,
637
+
});
638
+
} else {
639
+
// Handle non-object output schemas (like refs) by creating a type alias
640
+
const outputType = convertLexiconTypeToTypeScript(
641
+
defValue.output.schema,
642
+
lexicon.id,
643
+
undefined,
644
+
lexicons
645
+
);
843
646
844
-
sourceFile.addInterface({
845
-
name: "OAuthOperationError",
846
-
isExported: true,
847
-
properties: [
848
-
{ name: "success", type: "false" },
849
-
{ name: "message", type: "string" },
850
-
],
851
-
});
647
+
sourceFile.addTypeAlias({
648
+
name: interfaceName,
649
+
isExported: true,
650
+
type: outputType,
651
+
});
652
+
}
852
653
}
853
654
}
854
655
855
656
export function generateInterfaces(
856
657
sourceFile: SourceFile,
857
-
lexicons: Lexicon[],
858
-
excludeSlicesClient: boolean
658
+
lexicons: Lexicon[]
859
659
): void {
860
-
// Base interfaces are imported from @slices/client, only add network.slices specific interfaces
861
-
addBaseInterfaces(sourceFile, excludeSlicesClient);
660
+
// Base interfaces are imported from @slices/client, only add custom interfaces
661
+
addBaseInterfaces(sourceFile);
862
662
863
663
// Generate type aliases for string fields with knownValues
864
664
generateKnownValuesTypes(sourceFile, lexicons);
···
896
696
break;
897
697
case "token":
898
698
generateTokenType(sourceFile, lexicon, defKey);
699
+
break;
700
+
case "query":
701
+
generateQueryProcedureInterfaces(
702
+
sourceFile,
703
+
lexicon,
704
+
defValue,
705
+
lexicons,
706
+
"query"
707
+
);
708
+
break;
709
+
case "procedure":
710
+
generateQueryProcedureInterfaces(
711
+
sourceFile,
712
+
lexicon,
713
+
defValue,
714
+
lexicons,
715
+
"procedure"
716
+
);
899
717
break;
900
718
}
901
719
}
+25
-14
packages/codegen/src/mod.ts
+25
-14
packages/codegen/src/mod.ts
···
19
19
required?: string[];
20
20
}
21
21
22
+
export interface LexiconParameters {
23
+
type: "params";
24
+
properties?: Record<string, LexiconProperty>;
25
+
required?: string[];
26
+
}
27
+
28
+
export interface LexiconIO {
29
+
encoding: string;
30
+
schema?: LexiconProperty;
31
+
}
32
+
22
33
export interface LexiconDefinition {
23
34
type: string;
35
+
description?: string;
36
+
// Record type fields
24
37
record?: LexiconRecord;
38
+
key?: string;
39
+
// Query/Procedure fields
40
+
parameters?: LexiconParameters;
41
+
input?: LexiconIO;
42
+
output?: LexiconIO;
43
+
// Generic schema fields
25
44
properties?: Record<string, LexiconProperty>;
26
45
required?: string[];
27
46
refs?: string[];
···
39
58
40
59
export interface GenerateOptions {
41
60
sliceUri: string;
42
-
excludeSlicesClient?: boolean;
43
61
}
44
62
45
63
// Normalize lexicons to use consistent property names
···
268
286
269
287
export function generateHeaderComment(
270
288
lexicons: Lexicon[],
271
-
usageExample: string,
272
-
excludeSlicesClient: boolean
289
+
usageExample: string
273
290
): string {
274
291
return `// Generated TypeScript client for AT Protocol records
275
292
// Generated at: ${new Date().toISOString().slice(0, 19).replace("T", " ")} UTC
···
277
294
278
295
${usageExample}
279
296
280
-
${
281
-
excludeSlicesClient
282
-
? 'import { SlicesClient, type RecordResponse, type GetRecordsResponse, type CountRecordsResponse, type GetRecordParams, type WhereCondition, type IndexedRecordFields, type SortField, type BlobRef, type AuthProvider } from "@slices/client";\nimport type { OAuthClient } from "@slices/oauth";'
283
-
: 'import { SlicesClient, type RecordResponse, type GetRecordsResponse, type CountRecordsResponse, type GetRecordParams, type WhereCondition, type IndexedRecordFields, type SortField, type GetActorsParams, type GetActorsResponse, type BlobRef, type SliceLevelRecordsParams, type SliceRecordsOutput, type AuthProvider } from "@slices/client";\nimport type { OAuthClient } from "@slices/oauth";'
284
-
}
297
+
import { SlicesClient, type RecordResponse, type GetRecordsResponse, type CountRecordsResponse, type GetRecordParams, type WhereCondition, type IndexedRecordFields, type SortField, type BlobRef, type AuthProvider } from "@slices/client";
298
+
import type { OAuthClient } from "@slices/oauth";
285
299
286
300
`;
287
301
}
···
338
352
);
339
353
const headerComment = generateHeaderComment(
340
354
normalizedLexicons,
341
-
usageExample,
342
-
options.excludeSlicesClient || false
355
+
usageExample
343
356
);
344
357
345
358
// Add header comment and imports to the source file first
···
348
361
// Generate interfaces and client
349
362
generateInterfaces(
350
363
sourceFile,
351
-
normalizedLexicons,
352
-
options.excludeSlicesClient || false
364
+
normalizedLexicons
353
365
);
354
366
generateClient(
355
367
sourceFile,
356
-
normalizedLexicons,
357
-
options.excludeSlicesClient || false
368
+
normalizedLexicons
358
369
);
359
370
360
371
// Get the generated code
+40
-26
packages/codegen/tests/client_test.ts
+40
-26
packages/codegen/tests/client_test.ts
···
28
28
},
29
29
];
30
30
31
-
generateClient(sourceFile, lexicons, true);
31
+
generateClient(sourceFile, lexicons);
32
32
const result = sourceFile.getFullText();
33
33
34
34
// Should create main AtProtoClient class
···
36
36
37
37
// Should create nested class structure
38
38
assertStringIncludes(result, "class ComClient");
39
-
assertStringIncludes(result, "class ExampleComClient");
39
+
assertStringIncludes(result, "class PostExampleComClient");
40
40
41
41
// Should have nested properties
42
42
assertStringIncludes(result, "readonly com: ComClient;");
43
-
assertStringIncludes(result, "readonly example: ExampleComClient;");
43
+
assertStringIncludes(result, "readonly post: PostExampleComClient;");
44
44
45
45
// Should have OAuth client property
46
-
assertStringIncludes(result, "readonly oauth?: OAuthClient;");
46
+
assertStringIncludes(result, "readonly oauth?: OAuthClient | AuthProvider;");
47
47
});
48
48
49
49
Deno.test("generateClient - creates CRUD methods for records", () => {
···
68
68
},
69
69
];
70
70
71
-
generateClient(sourceFile, lexicons, true);
71
+
generateClient(sourceFile, lexicons);
72
72
const result = sourceFile.getFullText();
73
73
74
74
// Should create CRUD methods
···
104
104
},
105
105
];
106
106
107
-
generateClient(sourceFile, lexicons, true);
107
+
generateClient(sourceFile, lexicons);
108
108
const result = sourceFile.getFullText();
109
109
110
110
// Main client constructor
111
-
assertStringIncludes(result, "constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient)");
111
+
assertStringIncludes(result, "constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient | AuthProvider)");
112
112
assertStringIncludes(result, "super(baseUrl, sliceUri, oauthClient);");
113
113
114
114
// Nested class constructors
···
116
116
assertStringIncludes(result, "this.client = client;");
117
117
});
118
118
119
-
Deno.test("generateClient - includes network.slices specific methods when not excluded", () => {
119
+
Deno.test("generateClient - creates client for network.slices lexicons", () => {
120
120
const project = createTestProject();
121
121
const sourceFile = project.createSourceFile("test.ts", "");
122
122
···
135
135
},
136
136
];
137
137
138
-
generateClient(sourceFile, lexicons, false);
138
+
generateClient(sourceFile, lexicons);
139
139
const result = sourceFile.getFullText();
140
140
141
-
// Should include network.slices specific methods
142
-
assertStringIncludes(result, "async stats(");
143
-
assertStringIncludes(result, "async getSparklines(");
144
-
assertStringIncludes(result, "async getSliceRecords<T = Record<string, unknown>>(");
145
-
assertStringIncludes(result, "async getActors(");
146
-
assertStringIncludes(result, "async startSync(");
147
-
assertStringIncludes(result, "async getJobStatus(");
148
-
assertStringIncludes(result, "async createOAuthClient(");
149
-
assertStringIncludes(result, "async syncUserCollections(");
141
+
// Should create proper client structure for network.slices
142
+
assertStringIncludes(result, "export class AtProtoClient extends SlicesClient");
143
+
assertStringIncludes(result, "class NetworkClient");
144
+
assertStringIncludes(result, "class SlicesNetworkClient");
145
+
assertStringIncludes(result, "readonly network: NetworkClient;");
146
+
assertStringIncludes(result, "readonly slices: SlicesNetworkClient;");
147
+
148
+
// Should include standard CRUD methods
149
+
assertStringIncludes(result, "async getRecords(");
150
+
assertStringIncludes(result, "async createRecord(");
150
151
});
151
152
152
-
Deno.test("generateClient - excludes network.slices methods when excluded", () => {
153
+
Deno.test("generateClient - handles mixed lexicon types", () => {
153
154
const project = createTestProject();
154
155
const sourceFile = project.createSourceFile("test.ts", "");
155
156
156
157
const lexicons: Lexicon[] = [
157
158
{
159
+
id: "app.bsky.feed.post",
160
+
definitions: {
161
+
main: {
162
+
type: "record",
163
+
record: {
164
+
type: "record",
165
+
properties: { text: { type: "string" } },
166
+
},
167
+
},
168
+
},
169
+
},
170
+
{
158
171
id: "network.slices.slice",
159
172
definitions: {
160
173
main: {
···
168
181
},
169
182
];
170
183
171
-
generateClient(sourceFile, lexicons, true);
184
+
generateClient(sourceFile, lexicons);
172
185
const result = sourceFile.getFullText();
173
186
174
-
// Should not include network.slices specific methods when excluded
175
-
assertEquals(result.includes("async codegen("), false);
176
-
assertEquals(result.includes("async getSliceRecords<T"), false);
177
-
assertEquals(result.includes("async createOAuthClient("), false);
187
+
// Should include both app.bsky and network.slices clients
188
+
assertStringIncludes(result, "class AppClient");
189
+
assertStringIncludes(result, "class NetworkClient");
190
+
assertStringIncludes(result, "readonly app: AppClient;");
191
+
assertStringIncludes(result, "readonly network: NetworkClient;");
178
192
});
179
193
180
194
Deno.test("generateClient - handles deep nesting correctly", () => {
···
208
222
},
209
223
];
210
224
211
-
generateClient(sourceFile, lexicons, true);
225
+
generateClient(sourceFile, lexicons);
212
226
const result = sourceFile.getFullText();
213
227
214
228
// Should create proper nesting: app.bsky.feed and app.bsky.actor
···
240
254
},
241
255
];
242
256
243
-
generateClient(sourceFile, lexicons, true);
257
+
generateClient(sourceFile, lexicons);
244
258
const result = sourceFile.getFullText();
245
259
246
260
// Should not create any client classes for non-record lexicons
+11
-20
packages/codegen/tests/integration_test.ts
+11
-20
packages/codegen/tests/integration_test.ts
···
60
60
61
61
const result = await generateTypeScript(lexicons, {
62
62
sliceUri: "at://did:example/com.example.slice/abc123",
63
-
excludeSlicesClient: false,
64
63
});
65
64
66
65
// Should include header comment with usage example
···
91
90
assertStringIncludes(result, "async getRecords(");
92
91
assertStringIncludes(result, "async createRecord(");
93
92
94
-
assertStringIncludes(result, "async getSliceRecords<T");
95
-
96
-
assertStringIncludes(result, "export interface BulkSyncParams");
93
+
// Should include standard client methods for all lexicons
97
94
98
95
// Code should be formatted (no obvious formatting issues)
99
96
assertEquals(result.includes(" ;"), false); // Double spaces before semicolons
100
97
assertEquals(result.includes("\t\t\t"), false); // Triple tabs
101
98
});
102
99
103
-
Deno.test("generateTypeScript - excludes slices client correctly", async () => {
100
+
Deno.test("generateTypeScript - generates client for standard lexicons", async () => {
104
101
const lexicons: Lexicon[] = [
105
102
{
106
103
id: "app.bsky.feed.post",
···
120
117
121
118
const result = await generateTypeScript(lexicons, {
122
119
sliceUri: "at://test/slice",
123
-
excludeSlicesClient: true,
124
120
});
125
121
126
-
// Should not include slice-specific imports
127
-
assertEquals(result.includes("SliceLevelRecordsParams"), false);
128
-
assertEquals(result.includes("GetActorsParams"), false);
129
-
130
-
assertEquals(result.includes("BulkSyncParams"), false);
131
-
132
-
assertEquals(result.includes("async getSliceRecords<T"), false);
133
-
134
-
// Should still include basic functionality
122
+
// Should include basic functionality
135
123
assertStringIncludes(result, "export interface AppBskyFeedPost");
136
124
assertStringIncludes(result, "export class AtProtoClient");
137
125
assertStringIncludes(result, "async getRecords(");
126
+
assertStringIncludes(result, "text?: string;");
127
+
128
+
// Should include imports
129
+
assertStringIncludes(result, 'SlicesClient');
130
+
assertStringIncludes(result, 'OAuthClient');
138
131
});
139
132
140
133
Deno.test("generateTypeScript - handles empty lexicons", async () => {
141
134
const result = await generateTypeScript([], {
142
135
sliceUri: "at://test/slice",
143
-
excludeSlicesClient: false,
144
136
});
145
137
146
138
// Should include basic structure even with no lexicons
···
148
140
assertStringIncludes(result, "Lexicons: 0");
149
141
assertStringIncludes(result, 'SlicesClient');
150
142
151
-
// Should include network.slices interfaces
152
-
assertStringIncludes(result, "export interface BulkSyncParams");
143
+
// Should include imports even with no lexicons
144
+
assertStringIncludes(result, "@slices/client");
153
145
154
146
// Should not create any client class (no records to work with)
155
147
assertEquals(result.includes("export class AtProtoClient"), false);
···
175
167
176
168
const result = await generateTypeScript(lexicons, {
177
169
sliceUri: "at://did:example/slice/123",
178
-
excludeSlicesClient: false,
179
170
});
180
171
181
172
// Should create usage example with the first non-network.slices lexicon
182
173
assertStringIncludes(result, "client.social.app.post.getRecords()");
183
174
assertStringIncludes(result, "at://did:example/slice/123");
184
175
assertStringIncludes(result, "social.app.post");
185
-
assertStringIncludes(result, "getSliceRecords<SocialAppPost>");
176
+
assertStringIncludes(result, "getRecords()");
186
177
});
+32
-16
packages/codegen/tests/interfaces_test.ts
+32
-16
packages/codegen/tests/interfaces_test.ts
···
31
31
},
32
32
];
33
33
34
-
generateInterfaces(sourceFile, lexicons, true);
34
+
generateInterfaces(sourceFile, lexicons);
35
35
const result = sourceFile.getFullText();
36
36
37
37
// Should create interface for the record
···
65
65
},
66
66
];
67
67
68
-
generateInterfaces(sourceFile, lexicons, true);
68
+
generateInterfaces(sourceFile, lexicons);
69
69
const result = sourceFile.getFullText();
70
70
71
71
assertStringIncludes(result, "export interface AppBskyEmbedDefsAspectRatio");
···
98
98
},
99
99
];
100
100
101
-
generateInterfaces(sourceFile, lexicons, true);
101
+
generateInterfaces(sourceFile, lexicons);
102
102
const result = sourceFile.getFullText();
103
103
104
104
assertStringIncludes(result, "export type AppBskyEmbedDefsView");
···
132
132
},
133
133
];
134
134
135
-
generateInterfaces(sourceFile, lexicons, true);
135
+
generateInterfaces(sourceFile, lexicons);
136
136
const result = sourceFile.getFullText();
137
137
138
138
assertStringIncludes(result, "export type ComExamplePostStatus");
···
162
162
},
163
163
];
164
164
165
-
generateInterfaces(sourceFile, lexicons, true);
165
+
generateInterfaces(sourceFile, lexicons);
166
166
const result = sourceFile.getFullText();
167
167
168
168
// Should create namespace interface for multiple definitions
···
171
171
assertStringIncludes(result, "readonly View: AppBskyEmbedDefsView;");
172
172
});
173
173
174
-
Deno.test("generateInterfaces - includes network.slices interfaces when not excluded", () => {
174
+
Deno.test("generateInterfaces - generates from network.slices lexicons", () => {
175
175
const project = createTestProject();
176
176
const sourceFile = project.createSourceFile("test.ts", "");
177
177
178
-
generateInterfaces(sourceFile, [], false);
178
+
const lexicons = [
179
+
{
180
+
id: "network.slices.slice",
181
+
definitions: {
182
+
main: {
183
+
type: "record",
184
+
record: {
185
+
type: "record",
186
+
properties: {
187
+
name: { type: "string" },
188
+
},
189
+
required: ["name"],
190
+
},
191
+
},
192
+
},
193
+
},
194
+
];
195
+
196
+
generateInterfaces(sourceFile, lexicons);
179
197
const result = sourceFile.getFullText();
180
198
181
-
assertStringIncludes(result, "export interface BulkSyncParams");
182
-
assertStringIncludes(result, "export interface JobStatus");
183
-
assertStringIncludes(result, "export interface OAuthClientDetails");
199
+
assertStringIncludes(result, "export interface NetworkSlicesSlice");
200
+
assertStringIncludes(result, "name: string;");
184
201
});
185
202
186
-
Deno.test("generateInterfaces - excludes network.slices interfaces when excluded", () => {
203
+
Deno.test("generateInterfaces - generates empty output for empty lexicons", () => {
187
204
const project = createTestProject();
188
205
const sourceFile = project.createSourceFile("test.ts", "");
189
206
190
-
generateInterfaces(sourceFile, [], true);
207
+
generateInterfaces(sourceFile, []);
191
208
const result = sourceFile.getFullText();
192
209
193
-
assertEquals(result.includes("CodegenXrpcRequest"), false);
194
-
assertEquals(result.includes("BulkSyncParams"), false);
195
-
assertEquals(result.includes("JobStatus"), false);
210
+
// Should have minimal output for empty lexicons
211
+
assertEquals(result.trim(), "");
196
212
});
197
213
198
214
Deno.test("generateInterfaces - handles single definition lexicons", () => {
···
215
231
},
216
232
];
217
233
218
-
generateInterfaces(sourceFile, lexicons, true);
234
+
generateInterfaces(sourceFile, lexicons);
219
235
const result = sourceFile.getFullText();
220
236
221
237
// Should use clean name for single definition
+6
-5
packages/codegen/tests/utils_test.ts
+6
-5
packages/codegen/tests/utils_test.ts
···
145
145
];
146
146
147
147
const usageExample = "/** Example usage */";
148
-
const result = generateHeaderComment(lexicons, usageExample, false);
148
+
const result = generateHeaderComment(lexicons, usageExample);
149
149
150
150
assertEquals(typeof result, "string");
151
151
assertEquals(result.includes("Generated TypeScript client"), true);
···
155
155
assertEquals(result.includes("@slices/oauth"), true);
156
156
});
157
157
158
-
Deno.test("generateHeaderComment - excludes slices client imports", () => {
158
+
Deno.test("generateHeaderComment - generates consistent imports", () => {
159
159
const lexicons: Lexicon[] = [];
160
160
const usageExample = "/** Example */";
161
-
const result = generateHeaderComment(lexicons, usageExample, true);
161
+
const result = generateHeaderComment(lexicons, usageExample);
162
162
163
-
assertEquals(result.includes("SliceLevelRecordsParams"), false);
164
-
assertEquals(result.includes("GetActorsParams"), false);
163
+
assertEquals(result.includes("SlicesClient"), true);
164
+
assertEquals(result.includes("@slices/client"), true);
165
+
assertEquals(result.includes("@slices/oauth"), true);
165
166
});