Skip to main content

oversync_api/
types.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use utoipa::ToSchema;
4
5fn deserialize_optional_json_value<'de, D>(
6	deserializer: D,
7) -> Result<Option<Option<serde_json::Value>>, D::Error>
8where
9	D: serde::Deserializer<'de>,
10{
11	Option::<serde_json::Value>::deserialize(deserializer).map(Some)
12}
13
14fn default_true() -> bool {
15	true
16}
17
18#[derive(Debug, Serialize, ToSchema)]
19pub struct HealthResponse {
20	pub status: &'static str,
21	pub version: &'static str,
22}
23
24#[derive(Debug, Clone, Serialize, ToSchema)]
25pub struct CycleInfo {
26	pub cycle_id: u64,
27	pub source: String,
28	pub query: String,
29	pub status: String,
30	pub started_at: DateTime<Utc>,
31	pub finished_at: Option<DateTime<Utc>>,
32	pub rows_created: u64,
33	pub rows_updated: u64,
34	pub rows_deleted: u64,
35	pub duration_ms: Option<u64>,
36	pub error: Option<String>,
37}
38
39#[derive(Debug, Serialize, ToSchema)]
40pub struct SinkInfo {
41	pub name: String,
42	pub sink_type: String,
43	#[serde(skip_serializing_if = "Option::is_none")]
44	pub config: Option<serde_json::Value>,
45}
46
47#[derive(Debug, Serialize, ToSchema)]
48pub struct ErrorResponse {
49	pub error: String,
50}
51
52#[derive(Debug, Serialize, ToSchema)]
53pub struct SinkListResponse {
54	pub sinks: Vec<SinkInfo>,
55}
56
57// ── Mutation request types ──────────────────────────────────
58
59#[derive(Debug, Deserialize, ToSchema)]
60pub struct CreateSinkRequest {
61	pub name: String,
62	pub sink_type: String,
63	#[serde(default)]
64	pub config: serde_json::Value,
65}
66
67#[derive(Debug, Deserialize, ToSchema)]
68pub struct UpdateSinkRequest {
69	pub sink_type: Option<String>,
70	pub config: Option<serde_json::Value>,
71	pub enabled: Option<bool>,
72}
73
74#[derive(Debug, Serialize, ToSchema)]
75pub struct MutationResponse {
76	pub ok: bool,
77	pub message: String,
78}
79
80#[derive(Debug, Serialize, ToSchema)]
81pub struct HistoryResponse {
82	pub cycles: Vec<CycleInfo>,
83}
84
85#[derive(Debug, Serialize, ToSchema)]
86pub struct StatusResponse {
87	pub running: bool,
88	pub paused: bool,
89}
90
91#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)]
92#[serde(rename_all = "lowercase")]
93pub enum ExportConfigFormat {
94	Toml,
95	Json,
96}
97
98#[derive(Debug, Deserialize, ToSchema)]
99pub struct ExportConfigQuery {
100	pub format: Option<ExportConfigFormat>,
101}
102
103#[derive(Debug, Serialize, ToSchema)]
104pub struct ExportConfigResponse {
105	pub format: ExportConfigFormat,
106	pub content: String,
107}
108
109#[derive(Debug, Deserialize, ToSchema)]
110pub struct ImportConfigRequest {
111	pub format: ExportConfigFormat,
112	pub content: String,
113}
114
115#[derive(Debug, Serialize, ToSchema)]
116pub struct ImportConfigResponse {
117	pub ok: bool,
118	pub message: String,
119	pub warnings: Vec<String>,
120}
121
122// ── Pipe types ──────────────────────────────────────────────
123
124#[derive(Debug, Serialize, ToSchema)]
125pub struct PipeListResponse {
126	pub pipes: Vec<PipeInfo>,
127}
128
129#[derive(Debug, Serialize, ToSchema)]
130pub struct PipePresetListResponse {
131	pub presets: Vec<PipePresetInfo>,
132}
133
134#[derive(Debug, Serialize, ToSchema)]
135pub struct PipeInfo {
136	pub name: String,
137	pub origin_connector: String,
138	pub origin_dsn: String,
139	pub targets: Vec<String>,
140	pub interval_secs: u64,
141	pub query_count: usize,
142	#[serde(skip_serializing_if = "Option::is_none")]
143	pub recipe: Option<serde_json::Value>,
144	pub enabled: bool,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
148pub struct PipePresetSpecInput {
149	pub origin_connector: String,
150	pub origin_dsn: String,
151	#[serde(default)]
152	pub origin_credential: Option<String>,
153	#[serde(default)]
154	pub trino_url: Option<String>,
155	#[serde(default)]
156	pub origin_config: serde_json::Value,
157	#[serde(default)]
158	pub parameters: Vec<PipePresetParameterInput>,
159	#[serde(default)]
160	pub targets: Vec<String>,
161	#[serde(default)]
162	pub queries: Vec<PipeQueryInput>,
163	#[serde(default)]
164	pub schedule: serde_json::Value,
165	#[serde(default)]
166	pub delta: serde_json::Value,
167	#[serde(default)]
168	pub retry: serde_json::Value,
169	#[serde(default)]
170	pub recipe: Option<serde_json::Value>,
171	#[serde(default)]
172	pub filters: Vec<serde_json::Value>,
173	#[serde(default)]
174	pub transforms: Vec<serde_json::Value>,
175	#[serde(default)]
176	pub links: Vec<serde_json::Value>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
180pub struct PipePresetParameterInput {
181	pub name: String,
182	#[serde(default)]
183	pub label: Option<String>,
184	#[serde(default)]
185	pub description: Option<String>,
186	#[serde(default)]
187	pub default: Option<String>,
188	#[serde(default = "default_true")]
189	pub required: bool,
190	#[serde(default)]
191	pub secret: bool,
192}
193
194#[derive(Debug, Clone, Serialize, ToSchema)]
195pub struct PipePresetInfo {
196	pub name: String,
197	#[serde(skip_serializing_if = "Option::is_none")]
198	pub description: Option<String>,
199	pub spec: serde_json::Value,
200}
201
202#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
203pub struct PipeQueryInput {
204	pub id: String,
205	pub sql: String,
206	pub key_column: String,
207	#[serde(default)]
208	pub sinks: Option<Vec<String>>,
209	#[serde(default)]
210	pub transform: Option<String>,
211}
212
213#[derive(Debug, Deserialize, ToSchema)]
214pub struct CreatePipeRequest {
215	pub name: String,
216	pub origin_connector: String,
217	pub origin_dsn: String,
218	#[serde(default)]
219	pub origin_credential: Option<String>,
220	#[serde(default)]
221	pub trino_url: Option<String>,
222	#[serde(default)]
223	pub origin_config: serde_json::Value,
224	#[serde(default)]
225	pub targets: Vec<String>,
226	#[serde(default)]
227	pub schedule: serde_json::Value,
228	#[serde(default)]
229	pub delta: serde_json::Value,
230	#[serde(default)]
231	pub retry: serde_json::Value,
232	#[serde(default)]
233	pub recipe: Option<serde_json::Value>,
234	#[serde(default)]
235	pub filters: Vec<serde_json::Value>,
236	#[serde(default)]
237	pub transforms: Vec<serde_json::Value>,
238	#[serde(default)]
239	pub links: Vec<serde_json::Value>,
240	#[serde(default)]
241	pub queries: Vec<PipeQueryInput>,
242}
243
244#[derive(Debug, Deserialize, ToSchema)]
245pub struct UpdatePipeRequest {
246	pub origin_connector: Option<String>,
247	pub origin_dsn: Option<String>,
248	pub origin_credential: Option<String>,
249	pub trino_url: Option<String>,
250	pub origin_config: Option<serde_json::Value>,
251	pub targets: Option<Vec<String>>,
252	pub schedule: Option<serde_json::Value>,
253	pub delta: Option<serde_json::Value>,
254	pub retry: Option<serde_json::Value>,
255	#[serde(default, deserialize_with = "deserialize_optional_json_value")]
256	pub recipe: Option<Option<serde_json::Value>>,
257	pub filters: Option<Vec<serde_json::Value>>,
258	pub transforms: Option<Vec<serde_json::Value>>,
259	pub links: Option<Vec<serde_json::Value>>,
260	pub queries: Option<Vec<PipeQueryInput>>,
261	pub enabled: Option<bool>,
262}
263
264#[derive(Debug, Deserialize, ToSchema)]
265pub struct CreatePipePresetRequest {
266	pub name: String,
267	#[serde(default)]
268	pub description: Option<String>,
269	pub spec: PipePresetSpecInput,
270}
271
272#[derive(Debug, Deserialize, ToSchema)]
273pub struct UpdatePipePresetRequest {
274	#[serde(default)]
275	pub description: Option<String>,
276	pub spec: Option<PipePresetSpecInput>,
277}
278
279// ── Credential types ────────────────────────────────────────
280
281#[derive(Debug, Serialize, ToSchema)]
282pub struct CredentialListResponse {
283	pub credentials: Vec<CredentialInfo>,
284}
285
286#[derive(Debug, Serialize, ToSchema)]
287pub struct CredentialInfo {
288	pub name: String,
289	pub credential_type: String,
290	pub created_at: String,
291}
292
293#[derive(Debug, Deserialize, ToSchema)]
294pub struct CreateCredentialRequest {
295	pub name: String,
296	pub credential_type: String,
297	pub secret: String,
298}
299
300#[derive(Debug, Deserialize, ToSchema)]
301pub struct UpdateCredentialRequest {
302	pub secret: Option<String>,
303	pub credential_type: Option<String>,
304}
305
306#[cfg(test)]
307mod tests {
308	use super::UpdatePipeRequest;
309
310	#[test]
311	fn update_pipe_request_distinguishes_null_recipe_from_omitted() {
312		let cleared: UpdatePipeRequest = serde_json::from_value(serde_json::json!({
313			"recipe": null
314		}))
315		.expect("request with null recipe should deserialize");
316		assert!(matches!(cleared.recipe, Some(None)));
317
318		let set: UpdatePipeRequest = serde_json::from_value(serde_json::json!({
319			"recipe": {"type": "postgres_snapshot", "prefix": "demo"}
320		}))
321		.expect("request with recipe object should deserialize");
322		assert!(matches!(set.recipe, Some(Some(_))));
323
324		let omitted: UpdatePipeRequest = serde_json::from_value(serde_json::json!({}))
325			.expect("empty request should deserialize");
326		assert!(omitted.recipe.is_none());
327	}
328}