1use std::collections::HashMap;
4
5use serde_json::{Map as JsonMap, Value as JsonValue};
6
7use crate::expr::RelationFilterOp;
8use crate::{BinaryOp, Error, Expr, FindManyArgs, IncludeRelation, OrderBy, OrderDir, Result};
9
10pub fn find_many_args_to_protocol_json(args: &FindManyArgs) -> Result<JsonValue> {
17 let mut result = JsonMap::new();
18
19 if let Some(where_) = &args.where_ {
20 result.insert("where".to_string(), expr_to_filter_json(where_)?);
21 }
22
23 if !args.order_by.is_empty() {
24 result.insert(
25 "orderBy".to_string(),
26 JsonValue::Array(
27 args.order_by
28 .iter()
29 .map(order_by_to_json)
30 .collect::<Result<Vec<_>>>()?,
31 ),
32 );
33 }
34
35 if let Some(take) = args.take {
36 result.insert("take".to_string(), JsonValue::from(take));
37 }
38
39 if let Some(skip) = args.skip {
40 result.insert("skip".to_string(), JsonValue::from(skip));
41 }
42
43 if !args.include.is_empty() {
44 result.insert("include".to_string(), include_map_to_json(&args.include)?);
45 }
46
47 if !args.select.is_empty() {
48 let mut select = JsonMap::new();
49 for (field, enabled) in &args.select {
50 select.insert(field.clone(), JsonValue::Bool(*enabled));
51 }
52 result.insert("select".to_string(), JsonValue::Object(select));
53 }
54
55 if let Some(cursor) = &args.cursor {
56 let mut wire_cursor = JsonMap::new();
57 for (field, value) in cursor {
58 wire_cursor.insert(strip_column_qualifier(field), value.to_json_plain());
59 }
60 result.insert("cursor".to_string(), JsonValue::Object(wire_cursor));
61 }
62
63 if !args.distinct.is_empty() {
64 result.insert(
65 "distinct".to_string(),
66 JsonValue::Array(
67 args.distinct
68 .iter()
69 .map(|field| JsonValue::String(strip_column_qualifier(field)))
70 .collect(),
71 ),
72 );
73 }
74
75 Ok(JsonValue::Object(result))
76}
77
78pub fn where_expr_to_protocol_json(expr: &Expr) -> Result<JsonValue> {
80 expr_to_filter_json(expr)
81}
82
83fn include_map_to_json(include: &HashMap<String, IncludeRelation>) -> Result<JsonValue> {
84 let mut result = JsonMap::new();
85 for (field, relation) in include {
86 result.insert(field.clone(), include_relation_to_json(relation)?);
87 }
88 Ok(JsonValue::Object(result))
89}
90
91fn include_relation_to_json(include: &IncludeRelation) -> Result<JsonValue> {
92 let mut result = JsonMap::new();
93
94 if let Some(where_) = &include.where_ {
95 result.insert("where".to_string(), expr_to_filter_json(where_)?);
96 }
97
98 if !include.order_by.is_empty() {
99 result.insert(
100 "orderBy".to_string(),
101 JsonValue::Array(
102 include
103 .order_by
104 .iter()
105 .map(order_by_to_json)
106 .collect::<Result<Vec<_>>>()?,
107 ),
108 );
109 }
110
111 if let Some(take) = include.take {
112 result.insert("take".to_string(), JsonValue::from(take));
113 }
114
115 if let Some(skip) = include.skip {
116 result.insert("skip".to_string(), JsonValue::from(skip));
117 }
118
119 if let Some(cursor) = &include.cursor {
120 let mut wire_cursor = JsonMap::new();
121 for (field, value) in cursor {
122 wire_cursor.insert(strip_column_qualifier(field), value.to_json_plain());
123 }
124 result.insert("cursor".to_string(), JsonValue::Object(wire_cursor));
125 }
126
127 if !include.distinct.is_empty() {
128 result.insert(
129 "distinct".to_string(),
130 JsonValue::Array(
131 include
132 .distinct
133 .iter()
134 .map(|field| JsonValue::String(strip_column_qualifier(field)))
135 .collect(),
136 ),
137 );
138 }
139
140 if !include.include.is_empty() {
141 result.insert(
142 "include".to_string(),
143 include_map_to_json(&include.include)?,
144 );
145 }
146
147 Ok(JsonValue::Object(result))
148}
149
150fn order_by_to_json(order: &OrderBy) -> Result<JsonValue> {
151 let mut result = JsonMap::new();
152 result.insert(
153 strip_column_qualifier(&order.column),
154 JsonValue::String(match order.direction {
155 OrderDir::Asc => "asc".to_string(),
156 OrderDir::Desc => "desc".to_string(),
157 }),
158 );
159 Ok(JsonValue::Object(result))
160}
161
162fn expr_to_filter_json(expr: &Expr) -> Result<JsonValue> {
163 match expr {
164 Expr::Binary {
165 left,
166 op: BinaryOp::And,
167 right,
168 } => logical_expr_to_json("AND", left, right),
169 Expr::Binary {
170 left,
171 op: BinaryOp::Or,
172 right,
173 } => logical_expr_to_json("OR", left, right),
174 Expr::Not(inner) => {
175 let mut result = JsonMap::new();
176 result.insert("NOT".to_string(), expr_to_filter_json(inner)?);
177 Ok(JsonValue::Object(result))
178 }
179 Expr::Relation { op, relation } => {
180 relation_predicate_to_json(&relation.field, *op, relation.filter.as_ref())
181 }
182 Expr::Binary { left, op, right } => field_predicate_to_json(left, op, right),
183 Expr::IsNull(inner) => null_predicate_to_json(inner, true),
184 Expr::IsNotNull(inner) => null_predicate_to_json(inner, false),
185 other => Err(Error::InvalidQuery(format!(
186 "query cannot be serialized to engine JSON: unsupported expression {:?}",
187 other
188 ))),
189 }
190}
191
192fn relation_predicate_to_json(
193 field: &str,
194 op: RelationFilterOp,
195 filter: &Expr,
196) -> Result<JsonValue> {
197 let mut relation_spec = JsonMap::new();
198 relation_spec.insert(
199 match op {
200 RelationFilterOp::Some => "some".to_string(),
201 RelationFilterOp::None => "none".to_string(),
202 RelationFilterOp::Every => "every".to_string(),
203 },
204 expr_to_filter_json(filter)?,
205 );
206
207 let mut result = JsonMap::new();
208 result.insert(field.to_string(), JsonValue::Object(relation_spec));
209 Ok(JsonValue::Object(result))
210}
211
212fn logical_expr_to_json(name: &str, left: &Expr, right: &Expr) -> Result<JsonValue> {
213 let mut items = Vec::new();
214 collect_logical_operands(name, left, &mut items)?;
215 collect_logical_operands(name, right, &mut items)?;
216
217 let mut result = JsonMap::new();
218 result.insert(name.to_string(), JsonValue::Array(items));
219 Ok(JsonValue::Object(result))
220}
221
222fn collect_logical_operands(name: &str, expr: &Expr, out: &mut Vec<JsonValue>) -> Result<()> {
223 match (name, expr) {
224 (
225 "AND",
226 Expr::Binary {
227 left,
228 op: BinaryOp::And,
229 right,
230 },
231 ) => {
232 collect_logical_operands(name, left, out)?;
233 collect_logical_operands(name, right, out)?;
234 Ok(())
235 }
236 (
237 "OR",
238 Expr::Binary {
239 left,
240 op: BinaryOp::Or,
241 right,
242 },
243 ) => {
244 collect_logical_operands(name, left, out)?;
245 collect_logical_operands(name, right, out)?;
246 Ok(())
247 }
248 _ => {
249 out.push(expr_to_filter_json(expr)?);
250 Ok(())
251 }
252 }
253}
254
255fn field_predicate_to_json(left: &Expr, op: &BinaryOp, right: &Expr) -> Result<JsonValue> {
256 let field = match left {
257 Expr::Column(name) => strip_column_qualifier(name),
258 other => {
259 return Err(Error::InvalidQuery(format!(
260 "query cannot be serialized to engine JSON: unsupported field operand {:?}",
261 other
262 )));
263 }
264 };
265
266 let (operator, value) = match op {
267 BinaryOp::Eq => (None, expr_value_to_json(right)?),
268 BinaryOp::Ne => (Some("ne"), expr_value_to_json(right)?),
269 BinaryOp::Lt => (Some("lt"), expr_value_to_json(right)?),
270 BinaryOp::Le => (Some("lte"), expr_value_to_json(right)?),
271 BinaryOp::Gt => (Some("gt"), expr_value_to_json(right)?),
272 BinaryOp::Ge => (Some("gte"), expr_value_to_json(right)?),
273 BinaryOp::Like => like_operator_and_value(right)?,
274 BinaryOp::In => (Some("in"), list_expr_to_json_array(right)?),
275 BinaryOp::NotIn => (Some("notIn"), list_expr_to_json_array(right)?),
276 other => {
277 return Err(Error::InvalidQuery(format!(
278 "query cannot be serialized to engine JSON: unsupported binary op {:?}",
279 other
280 )));
281 }
282 };
283
284 let mut result = JsonMap::new();
285 match operator {
286 None => {
287 result.insert(field, value);
288 }
289 Some(op_name) => {
290 let mut operators = JsonMap::new();
291 operators.insert(op_name.to_string(), value);
292 result.insert(field, JsonValue::Object(operators));
293 }
294 }
295
296 Ok(JsonValue::Object(result))
297}
298
299fn null_predicate_to_json(inner: &Expr, is_null: bool) -> Result<JsonValue> {
300 let field = match inner {
301 Expr::Column(name) => strip_column_qualifier(name),
302 other => {
303 return Err(Error::InvalidQuery(format!(
304 "query cannot be serialized to engine JSON: unsupported null predicate {:?}",
305 other
306 )));
307 }
308 };
309
310 let mut operators = JsonMap::new();
311 operators.insert("isNull".to_string(), JsonValue::Bool(is_null));
312
313 let mut result = JsonMap::new();
314 result.insert(field, JsonValue::Object(operators));
315 Ok(JsonValue::Object(result))
316}
317
318fn like_operator_and_value(expr: &Expr) -> Result<(Option<&'static str>, JsonValue)> {
319 let value = match expr {
320 Expr::Param(value) => value.to_json_plain(),
321 other => {
322 return Err(Error::InvalidQuery(format!(
323 "query cannot be serialized to engine JSON: unsupported LIKE operand {:?}",
324 other
325 )));
326 }
327 };
328
329 let Some(pattern) = value.as_str() else {
330 return Ok((Some("like"), value));
331 };
332
333 if pattern.starts_with('%') && pattern.ends_with('%') && pattern.len() >= 2 {
334 return Ok((
335 Some("contains"),
336 JsonValue::String(pattern[1..pattern.len() - 1].to_string()),
337 ));
338 }
339
340 if let Some(stripped) = pattern.strip_prefix('%') {
341 return Ok((Some("endsWith"), JsonValue::String(stripped.to_string())));
342 }
343
344 if let Some(stripped) = pattern.strip_suffix('%') {
345 return Ok((Some("startsWith"), JsonValue::String(stripped.to_string())));
346 }
347
348 Ok((Some("like"), JsonValue::String(pattern.to_string())))
349}
350
351fn list_expr_to_json_array(expr: &Expr) -> Result<JsonValue> {
352 let Expr::List(items) = expr else {
353 return Err(Error::InvalidQuery(format!(
354 "query cannot be serialized to engine JSON: unsupported list operand {:?}",
355 expr
356 )));
357 };
358
359 Ok(JsonValue::Array(
360 items
361 .iter()
362 .map(expr_value_to_json)
363 .collect::<Result<Vec<_>>>()?,
364 ))
365}
366
367fn expr_value_to_json(expr: &Expr) -> Result<JsonValue> {
368 match expr {
369 Expr::Param(value) => Ok(value.to_json_plain()),
370 other => Err(Error::InvalidQuery(format!(
371 "query cannot be serialized to engine JSON: unsupported value expression {:?}",
372 other
373 ))),
374 }
375}
376
377fn strip_column_qualifier(name: &str) -> String {
378 name.split_once("__")
379 .map(|(_, column)| column.to_string())
380 .unwrap_or_else(|| name.to_string())
381}
382
383#[cfg(test)]
384mod tests {
385 use std::collections::HashMap;
386
387 use serde_json::json;
388
389 use super::*;
390 use crate::{Column, Value};
391
392 #[test]
393 fn find_many_args_serializes_supported_filters_and_includes() {
394 let args = FindManyArgs {
395 where_: Some(
396 Column::<String>::new("Entry", "slug")
397 .contains("rust-entry-")
398 .and(Expr::column("Entry__id").gt(Expr::param(2))),
399 ),
400 order_by: vec![Column::<i32>::new("Entry", "id").asc()],
401 take: Some(2),
402 skip: Some(1),
403 include: HashMap::from([(
404 "author".to_string(),
405 IncludeRelation::with_filter(
406 Column::<String>::new("User", "email").eq("a@example.com"),
407 )
408 .with_order_by(Column::<i32>::new("User", "id").desc())
409 .with_take(1)
410 .with_include("posts", IncludeRelation::plain()),
411 )]),
412 select: HashMap::from([("id".to_string(), true), ("slug".to_string(), true)]),
413 cursor: Some(HashMap::from([("id".to_string(), Value::I32(10))])),
414 distinct: vec!["Entry__slug".to_string()],
415 };
416
417 let json = find_many_args_to_protocol_json(&args).expect("serialization should succeed");
418
419 assert_eq!(
420 json,
421 json!({
422 "where": {
423 "AND": [
424 { "slug": { "contains": "rust-entry-" } },
425 { "id": { "gt": 2 } }
426 ]
427 },
428 "orderBy": [{ "id": "asc" }],
429 "take": 2,
430 "skip": 1,
431 "include": {
432 "author": {
433 "where": { "email": "a@example.com" },
434 "orderBy": [{ "id": "desc" }],
435 "take": 1,
436 "include": {
437 "posts": {}
438 }
439 }
440 },
441 "select": {
442 "id": true,
443 "slug": true
444 },
445 "cursor": {
446 "id": 10
447 },
448 "distinct": ["slug"]
449 })
450 );
451 }
452
453 #[test]
454 fn unsupported_expression_returns_invalid_query() {
455 let args = FindManyArgs {
456 where_: Some(Expr::exists(
457 crate::Select::from_table("Post")
458 .filter(Expr::column("Post__user_id").eq(Expr::column("User__id")))
459 .build()
460 .expect("valid select"),
461 )),
462 ..Default::default()
463 };
464
465 let err = find_many_args_to_protocol_json(&args).expect_err("exists is not serializable");
466 assert!(matches!(err, Error::InvalidQuery(_)));
467 }
468
469 #[test]
470 fn relation_predicates_serialize_to_relation_where_objects() {
471 let args = FindManyArgs {
472 where_: Some(
473 Expr::relation_some(
474 "posts",
475 "User",
476 "Post",
477 "user_id",
478 "id",
479 crate::Column::<String>::new("Post", "title")
480 .contains("rust")
481 .and(Expr::relation_none(
482 "comments",
483 "Post",
484 "Comment",
485 "post_id",
486 "id",
487 crate::Column::<bool>::new("Comment", "flagged").eq(false),
488 )),
489 )
490 .and(Expr::relation_every(
491 "posts",
492 "User",
493 "Post",
494 "user_id",
495 "id",
496 crate::Column::<bool>::new("Post", "published").eq(true),
497 )),
498 ),
499 ..Default::default()
500 };
501
502 let json =
503 find_many_args_to_protocol_json(&args).expect("relation filters should serialize");
504
505 assert_eq!(
506 json,
507 json!({
508 "where": {
509 "AND": [
510 {
511 "posts": {
512 "some": {
513 "AND": [
514 { "title": { "contains": "rust" } },
515 { "comments": { "none": { "flagged": false } } }
516 ]
517 }
518 }
519 },
520 {
521 "posts": {
522 "every": {
523 "published": true
524 }
525 }
526 }
527 ]
528 }
529 })
530 );
531 }
532}