1use std::collections::HashSet;
6
7use anyhow::Result;
8use tracing::{debug, info};
9
10use super::intermediate::IntermediateSchema;
11
12#[derive(Debug, Clone)]
14pub struct ValidationError {
15 pub message: String,
17 pub path: String,
19 pub severity: ErrorSeverity,
21 pub suggestion: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ErrorSeverity {
28 Error,
30 Warning,
32}
33
34pub struct SchemaValidator;
36
37impl SchemaValidator {
38 pub fn validate(schema: &IntermediateSchema) -> Result<ValidationReport> {
40 info!("Validating schema structure");
41
42 let mut report = ValidationReport::default();
43
44 let mut type_names = HashSet::new();
46 for type_def in &schema.types {
47 if type_names.contains(&type_def.name) {
48 report.errors.push(ValidationError {
49 message: format!("Duplicate type name: '{}'", type_def.name),
50 path: format!("types[{}].name", type_names.len()),
51 severity: ErrorSeverity::Error,
52 suggestion: Some("Type names must be unique".to_string()),
53 });
54 }
55 type_names.insert(type_def.name.clone());
56 }
57
58 type_names.insert("Int".to_string());
60 type_names.insert("Float".to_string());
61 type_names.insert("String".to_string());
62 type_names.insert("Boolean".to_string());
63 type_names.insert("ID".to_string());
64
65 let mut query_names = HashSet::new();
67 for (idx, query) in schema.queries.iter().enumerate() {
68 debug!("Validating query: {}", query.name);
69
70 if query_names.contains(&query.name) {
72 report.errors.push(ValidationError {
73 message: format!("Duplicate query name: '{}'", query.name),
74 path: format!("queries[{idx}].name"),
75 severity: ErrorSeverity::Error,
76 suggestion: Some("Query names must be unique".to_string()),
77 });
78 }
79 query_names.insert(query.name.clone());
80
81 if !type_names.contains(&query.return_type) {
83 report.errors.push(ValidationError {
84 message: format!(
85 "Query '{}' references unknown type '{}'",
86 query.name, query.return_type
87 ),
88 path: format!("queries[{idx}].return_type"),
89 severity: ErrorSeverity::Error,
90 suggestion: Some(format!(
91 "Available types: {}",
92 Self::suggest_similar_type(&query.return_type, &type_names)
93 )),
94 });
95 }
96
97 for (arg_idx, arg) in query.arguments.iter().enumerate() {
99 if !type_names.contains(&arg.arg_type) {
100 report.errors.push(ValidationError {
101 message: format!(
102 "Query '{}' argument '{}' references unknown type '{}'",
103 query.name, arg.name, arg.arg_type
104 ),
105 path: format!("queries[{idx}].arguments[{arg_idx}].type"),
106 severity: ErrorSeverity::Error,
107 suggestion: Some(format!(
108 "Available types: {}",
109 Self::suggest_similar_type(&arg.arg_type, &type_names)
110 )),
111 });
112 }
113 }
114
115 if query.sql_source.is_none() && query.returns_list {
117 report.errors.push(ValidationError {
118 message: format!(
119 "Query '{}' returns a list but has no sql_source",
120 query.name
121 ),
122 path: format!("queries[{idx}]"),
123 severity: ErrorSeverity::Warning,
124 suggestion: Some("Add sql_source for SQL-backed queries".to_string()),
125 });
126 }
127 }
128
129 let mut mutation_names = HashSet::new();
131 for (idx, mutation) in schema.mutations.iter().enumerate() {
132 debug!("Validating mutation: {}", mutation.name);
133
134 if mutation_names.contains(&mutation.name) {
136 report.errors.push(ValidationError {
137 message: format!("Duplicate mutation name: '{}'", mutation.name),
138 path: format!("mutations[{idx}].name"),
139 severity: ErrorSeverity::Error,
140 suggestion: Some("Mutation names must be unique".to_string()),
141 });
142 }
143 mutation_names.insert(mutation.name.clone());
144
145 if !type_names.contains(&mutation.return_type) {
147 report.errors.push(ValidationError {
148 message: format!(
149 "Mutation '{}' references unknown type '{}'",
150 mutation.name, mutation.return_type
151 ),
152 path: format!("mutations[{idx}].return_type"),
153 severity: ErrorSeverity::Error,
154 suggestion: Some(format!(
155 "Available types: {}",
156 Self::suggest_similar_type(&mutation.return_type, &type_names)
157 )),
158 });
159 }
160
161 for (arg_idx, arg) in mutation.arguments.iter().enumerate() {
163 if !type_names.contains(&arg.arg_type) {
164 report.errors.push(ValidationError {
165 message: format!(
166 "Mutation '{}' argument '{}' references unknown type '{}'",
167 mutation.name, arg.name, arg.arg_type
168 ),
169 path: format!("mutations[{idx}].arguments[{arg_idx}].type"),
170 severity: ErrorSeverity::Error,
171 suggestion: Some(format!(
172 "Available types: {}",
173 Self::suggest_similar_type(&arg.arg_type, &type_names)
174 )),
175 });
176 }
177 }
178 }
179
180 if let Some(observers) = &schema.observers {
182 let mut observer_names = HashSet::new();
183 for (idx, observer) in observers.iter().enumerate() {
184 debug!("Validating observer: {}", observer.name);
185
186 if observer_names.contains(&observer.name) {
188 report.errors.push(ValidationError {
189 message: format!("Duplicate observer name: '{}'", observer.name),
190 path: format!("observers[{idx}].name"),
191 severity: ErrorSeverity::Error,
192 suggestion: Some("Observer names must be unique".to_string()),
193 });
194 }
195 observer_names.insert(observer.name.clone());
196
197 if !type_names.contains(&observer.entity) {
199 report.errors.push(ValidationError {
200 message: format!(
201 "Observer '{}' references unknown entity '{}'",
202 observer.name, observer.entity
203 ),
204 path: format!("observers[{idx}].entity"),
205 severity: ErrorSeverity::Error,
206 suggestion: Some(format!(
207 "Available types: {}",
208 Self::suggest_similar_type(&observer.entity, &type_names)
209 )),
210 });
211 }
212
213 let valid_events = ["INSERT", "UPDATE", "DELETE"];
215 if !valid_events.contains(&observer.event.as_str()) {
216 report.errors.push(ValidationError {
217 message: format!(
218 "Observer '{}' has invalid event '{}'. Must be INSERT, UPDATE, or DELETE",
219 observer.name, observer.event
220 ),
221 path: format!("observers[{idx}].event"),
222 severity: ErrorSeverity::Error,
223 suggestion: Some("Valid events: INSERT, UPDATE, DELETE".to_string()),
224 });
225 }
226
227 if observer.actions.is_empty() {
229 report.errors.push(ValidationError {
230 message: format!(
231 "Observer '{}' must have at least one action",
232 observer.name
233 ),
234 path: format!("observers[{idx}].actions"),
235 severity: ErrorSeverity::Error,
236 suggestion: Some("Add a webhook, slack, or email action".to_string()),
237 });
238 }
239
240 for (action_idx, action) in observer.actions.iter().enumerate() {
242 if let Some(obj) = action.as_object() {
243 if let Some(action_type) = obj.get("type").and_then(|v| v.as_str()) {
245 let valid_action_types = ["webhook", "slack", "email"];
246 if !valid_action_types.contains(&action_type) {
247 report.errors.push(ValidationError {
248 message: format!(
249 "Observer '{}' action {} has invalid type '{}'",
250 observer.name, action_idx, action_type
251 ),
252 path: format!(
253 "observers[{idx}].actions[{action_idx}].type"
254 ),
255 severity: ErrorSeverity::Error,
256 suggestion: Some(
257 "Valid action types: webhook, slack, email".to_string(),
258 ),
259 });
260 }
261
262 match action_type {
264 "webhook" => {
265 let has_url = obj.contains_key("url");
266 let has_url_env = obj.contains_key("url_env");
267 if !has_url && !has_url_env {
268 report.errors.push(ValidationError {
269 message: format!(
270 "Observer '{}' webhook action must have 'url' or 'url_env'",
271 observer.name
272 ),
273 path: format!("observers[{idx}].actions[{action_idx}]"),
274 severity: ErrorSeverity::Error,
275 suggestion: Some("Add 'url' or 'url_env' field".to_string()),
276 });
277 }
278 },
279 "slack" => {
280 if !obj.contains_key("channel") {
281 report.errors.push(ValidationError {
282 message: format!(
283 "Observer '{}' slack action must have 'channel' field",
284 observer.name
285 ),
286 path: format!("observers[{idx}].actions[{action_idx}]"),
287 severity: ErrorSeverity::Error,
288 suggestion: Some("Add 'channel' field (e.g., '#sales')".to_string()),
289 });
290 }
291 if !obj.contains_key("message") {
292 report.errors.push(ValidationError {
293 message: format!(
294 "Observer '{}' slack action must have 'message' field",
295 observer.name
296 ),
297 path: format!("observers[{idx}].actions[{action_idx}]"),
298 severity: ErrorSeverity::Error,
299 suggestion: Some("Add 'message' field".to_string()),
300 });
301 }
302 },
303 "email" => {
304 let required_fields = ["to", "subject", "body"];
305 for field in &required_fields {
306 if !obj.contains_key(*field) {
307 report.errors.push(ValidationError {
308 message: format!(
309 "Observer '{}' email action must have '{}' field",
310 observer.name, field
311 ),
312 path: format!("observers[{idx}].actions[{action_idx}]"),
313 severity: ErrorSeverity::Error,
314 suggestion: Some(format!("Add '{field}' field")),
315 });
316 }
317 }
318 },
319 _ => {},
320 }
321 } else {
322 report.errors.push(ValidationError {
323 message: format!(
324 "Observer '{}' action {} missing 'type' field",
325 observer.name, action_idx
326 ),
327 path: format!("observers[{idx}].actions[{action_idx}]"),
328 severity: ErrorSeverity::Error,
329 suggestion: Some(
330 "Add 'type' field (webhook, slack, or email)".to_string(),
331 ),
332 });
333 }
334 } else {
335 report.errors.push(ValidationError {
336 message: format!(
337 "Observer '{}' action {} must be an object",
338 observer.name, action_idx
339 ),
340 path: format!("observers[{idx}].actions[{action_idx}]"),
341 severity: ErrorSeverity::Error,
342 suggestion: None,
343 });
344 }
345 }
346
347 let valid_backoff_strategies = ["exponential", "linear", "fixed"];
349 if !valid_backoff_strategies.contains(&observer.retry.backoff_strategy.as_str()) {
350 report.errors.push(ValidationError {
351 message: format!(
352 "Observer '{}' has invalid backoff_strategy '{}'",
353 observer.name, observer.retry.backoff_strategy
354 ),
355 path: format!("observers[{idx}].retry.backoff_strategy"),
356 severity: ErrorSeverity::Error,
357 suggestion: Some(
358 "Valid strategies: exponential, linear, fixed".to_string(),
359 ),
360 });
361 }
362
363 if observer.retry.max_attempts == 0 {
364 report.errors.push(ValidationError {
365 message: format!(
366 "Observer '{}' has max_attempts=0, actions will never execute",
367 observer.name
368 ),
369 path: format!("observers[{idx}].retry.max_attempts"),
370 severity: ErrorSeverity::Warning,
371 suggestion: Some("Set max_attempts >= 1".to_string()),
372 });
373 }
374
375 if observer.retry.initial_delay_ms == 0 {
376 report.errors.push(ValidationError {
377 message: format!(
378 "Observer '{}' has initial_delay_ms=0, retries will be immediate",
379 observer.name
380 ),
381 path: format!("observers[{idx}].retry.initial_delay_ms"),
382 severity: ErrorSeverity::Warning,
383 suggestion: Some("Consider setting initial_delay_ms > 0".to_string()),
384 });
385 }
386
387 if observer.retry.max_delay_ms < observer.retry.initial_delay_ms {
388 report.errors.push(ValidationError {
389 message: format!(
390 "Observer '{}' has max_delay_ms < initial_delay_ms",
391 observer.name
392 ),
393 path: format!("observers[{idx}].retry.max_delay_ms"),
394 severity: ErrorSeverity::Error,
395 suggestion: Some("max_delay_ms must be >= initial_delay_ms".to_string()),
396 });
397 }
398 }
399 }
400
401 info!(
402 "Validation complete: {} errors, {} warnings",
403 report.error_count(),
404 report.warning_count()
405 );
406
407 Ok(report)
408 }
409
410 fn suggest_similar_type(typo: &str, available: &HashSet<String>) -> String {
412 let similar: Vec<&String> = available
414 .iter()
415 .filter(|name| {
416 name.to_lowercase().starts_with(&typo[0..1].to_lowercase())
417 || typo.to_lowercase().starts_with(&name[0..1].to_lowercase())
418 })
419 .take(3)
420 .collect();
421
422 if similar.is_empty() {
423 available.iter().take(5).cloned().collect::<Vec<_>>().join(", ")
424 } else {
425 similar.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
426 }
427 }
428}
429
430#[derive(Debug, Default)]
432pub struct ValidationReport {
433 pub errors: Vec<ValidationError>,
435}
436
437impl ValidationReport {
438 pub fn is_valid(&self) -> bool {
440 !self.has_errors()
441 }
442
443 pub fn has_errors(&self) -> bool {
445 self.errors.iter().any(|e| e.severity == ErrorSeverity::Error)
446 }
447
448 pub fn error_count(&self) -> usize {
450 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Error).count()
451 }
452
453 pub fn warning_count(&self) -> usize {
455 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Warning).count()
456 }
457
458 pub fn print(&self) {
460 if self.errors.is_empty() {
461 return;
462 }
463
464 println!("\nš Validation Report:");
465
466 let errors: Vec<_> =
467 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Error).collect();
468
469 let warnings: Vec<_> =
470 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Warning).collect();
471
472 if !errors.is_empty() {
473 println!("\n ā Errors ({}):", errors.len());
474 for error in errors {
475 println!(" {}", error.message);
476 println!(" at: {}", error.path);
477 if let Some(suggestion) = &error.suggestion {
478 println!(" š” {suggestion}");
479 }
480 println!();
481 }
482 }
483
484 if !warnings.is_empty() {
485 println!("\n ā ļø Warnings ({}):", warnings.len());
486 for warning in warnings {
487 println!(" {}", warning.message);
488 println!(" at: {}", warning.path);
489 if let Some(suggestion) = &warning.suggestion {
490 println!(" š” {suggestion}");
491 }
492 println!();
493 }
494 }
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use crate::schema::intermediate::{IntermediateQuery, IntermediateType};
502
503 #[test]
504 fn test_validate_empty_schema() {
505 let schema = IntermediateSchema {
506 security: None,
507 version: "2.0.0".to_string(),
508 types: vec![],
509 enums: vec![],
510 input_types: vec![],
511 interfaces: vec![],
512 unions: vec![],
513 queries: vec![],
514 mutations: vec![],
515 subscriptions: vec![],
516 fragments: None,
517 directives: None,
518 fact_tables: None,
519 aggregate_queries: None,
520 observers: None,
521 custom_scalars: None,
522 observers_config: None,
523 federation_config: None,
524 };
525
526 let report = SchemaValidator::validate(&schema).unwrap();
527 assert!(report.is_valid());
528 }
529
530 #[test]
531 fn test_detect_unknown_return_type() {
532 let schema = IntermediateSchema {
533 security: None,
534 version: "2.0.0".to_string(),
535 types: vec![],
536 enums: vec![],
537 input_types: vec![],
538 interfaces: vec![],
539 unions: vec![],
540 queries: vec![IntermediateQuery {
541 name: "users".to_string(),
542 return_type: "UnknownType".to_string(),
543 returns_list: true,
544 nullable: false,
545 arguments: vec![],
546 description: None,
547 sql_source: Some("users".to_string()),
548 auto_params: None,
549 deprecated: None,
550 jsonb_column: None,
551 }],
552 mutations: vec![],
553 subscriptions: vec![],
554 fragments: None,
555 directives: None,
556 fact_tables: None,
557 aggregate_queries: None,
558 observers: None,
559 custom_scalars: None,
560 observers_config: None,
561 federation_config: None,
562 };
563
564 let report = SchemaValidator::validate(&schema).unwrap();
565 assert!(!report.is_valid());
566 assert_eq!(report.error_count(), 1);
567 assert!(report.errors[0].message.contains("unknown type 'UnknownType'"));
568 }
569
570 #[test]
571 fn test_detect_duplicate_query_names() {
572 let schema = IntermediateSchema {
573 security: None,
574 version: "2.0.0".to_string(),
575 types: vec![IntermediateType {
576 name: "User".to_string(),
577 fields: vec![],
578 description: None,
579 implements: vec![],
580 is_error: false,
581 }],
582 enums: vec![],
583 input_types: vec![],
584 interfaces: vec![],
585 unions: vec![],
586 queries: vec![
587 IntermediateQuery {
588 name: "users".to_string(),
589 return_type: "User".to_string(),
590 returns_list: true,
591 nullable: false,
592 arguments: vec![],
593 description: None,
594 sql_source: Some("users".to_string()),
595 auto_params: None,
596 deprecated: None,
597 jsonb_column: None,
598 },
599 IntermediateQuery {
600 name: "users".to_string(), return_type: "User".to_string(),
602 returns_list: true,
603 nullable: false,
604 arguments: vec![],
605 description: None,
606 sql_source: Some("users".to_string()),
607 auto_params: None,
608 deprecated: None,
609 jsonb_column: None,
610 },
611 ],
612 mutations: vec![],
613 subscriptions: vec![],
614 fragments: None,
615 directives: None,
616 fact_tables: None,
617 aggregate_queries: None,
618 observers: None,
619 custom_scalars: None,
620 observers_config: None,
621 federation_config: None,
622 };
623
624 let report = SchemaValidator::validate(&schema).unwrap();
625 assert!(!report.is_valid());
626 assert!(report.errors.iter().any(|e| e.message.contains("Duplicate query name")));
627 }
628
629 #[test]
630 fn test_warning_for_query_without_sql_source() {
631 let schema = IntermediateSchema {
632 security: None,
633 version: "2.0.0".to_string(),
634 types: vec![IntermediateType {
635 name: "User".to_string(),
636 fields: vec![],
637 description: None,
638 implements: vec![],
639 is_error: false,
640 }],
641 enums: vec![],
642 input_types: vec![],
643 interfaces: vec![],
644 unions: vec![],
645 queries: vec![IntermediateQuery {
646 name: "users".to_string(),
647 return_type: "User".to_string(),
648 returns_list: true,
649 nullable: false,
650 arguments: vec![],
651 description: None,
652 sql_source: None, auto_params: None,
654 deprecated: None,
655 jsonb_column: None,
656 }],
657 mutations: vec![],
658 subscriptions: vec![],
659 fragments: None,
660 directives: None,
661 fact_tables: None,
662 aggregate_queries: None,
663 observers: None,
664 custom_scalars: None,
665 observers_config: None,
666 federation_config: None,
667 };
668
669 let report = SchemaValidator::validate(&schema).unwrap();
670 assert!(report.is_valid()); assert_eq!(report.warning_count(), 1);
672 assert!(report.errors[0].message.contains("no sql_source"));
673 }
674
675 #[test]
676 fn test_valid_observer() {
677 use serde_json::json;
678
679 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
680
681 let schema = IntermediateSchema {
682 security: None,
683 version: "2.0.0".to_string(),
684 types: vec![IntermediateType {
685 name: "Order".to_string(),
686 fields: vec![],
687 description: None,
688 implements: vec![],
689 is_error: false,
690 }],
691 enums: vec![],
692 input_types: vec![],
693 interfaces: vec![],
694 unions: vec![],
695 queries: vec![],
696 mutations: vec![],
697 subscriptions: vec![],
698 fragments: None,
699 directives: None,
700 fact_tables: None,
701 aggregate_queries: None,
702 observers: Some(vec![IntermediateObserver {
703 name: "onOrderCreated".to_string(),
704 entity: "Order".to_string(),
705 event: "INSERT".to_string(),
706 actions: vec![json!({
707 "type": "webhook",
708 "url": "https://example.com/orders"
709 })],
710 condition: None,
711 retry: IntermediateRetryConfig {
712 max_attempts: 3,
713 backoff_strategy: "exponential".to_string(),
714 initial_delay_ms: 100,
715 max_delay_ms: 60000,
716 },
717 }]),
718 custom_scalars: None,
719 observers_config: None,
720 federation_config: None,
721 };
722
723 let report = SchemaValidator::validate(&schema).unwrap();
724 assert!(report.is_valid(), "Valid observer should pass validation");
725 assert_eq!(report.error_count(), 0);
726 }
727
728 #[test]
729 fn test_observer_with_unknown_entity() {
730 use serde_json::json;
731
732 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
733
734 let schema = IntermediateSchema {
735 security: None,
736 version: "2.0.0".to_string(),
737 types: vec![],
738 enums: vec![],
739 input_types: vec![],
740 interfaces: vec![],
741 unions: vec![],
742 queries: vec![],
743 mutations: vec![],
744 subscriptions: vec![],
745 fragments: None,
746 directives: None,
747 fact_tables: None,
748 aggregate_queries: None,
749 observers: Some(vec![IntermediateObserver {
750 name: "onOrderCreated".to_string(),
751 entity: "UnknownEntity".to_string(),
752 event: "INSERT".to_string(),
753 actions: vec![json!({"type": "webhook", "url": "https://example.com"})],
754 condition: None,
755 retry: IntermediateRetryConfig {
756 max_attempts: 3,
757 backoff_strategy: "exponential".to_string(),
758 initial_delay_ms: 100,
759 max_delay_ms: 60000,
760 },
761 }]),
762 custom_scalars: None,
763 observers_config: None,
764 federation_config: None,
765 };
766
767 let report = SchemaValidator::validate(&schema).unwrap();
768 assert!(!report.is_valid());
769 assert!(report.errors.iter().any(|e| e.message.contains("unknown entity")));
770 }
771
772 #[test]
773 fn test_observer_with_invalid_event() {
774 use serde_json::json;
775
776 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
777
778 let schema = IntermediateSchema {
779 security: None,
780 version: "2.0.0".to_string(),
781 types: vec![IntermediateType {
782 name: "Order".to_string(),
783 fields: vec![],
784 description: None,
785 implements: vec![],
786 is_error: false,
787 }],
788 enums: vec![],
789 input_types: vec![],
790 interfaces: vec![],
791 unions: vec![],
792 queries: vec![],
793 mutations: vec![],
794 subscriptions: vec![],
795 fragments: None,
796 directives: None,
797 fact_tables: None,
798 aggregate_queries: None,
799 observers: Some(vec![IntermediateObserver {
800 name: "onOrderCreated".to_string(),
801 entity: "Order".to_string(),
802 event: "INVALID_EVENT".to_string(),
803 actions: vec![json!({"type": "webhook", "url": "https://example.com"})],
804 condition: None,
805 retry: IntermediateRetryConfig {
806 max_attempts: 3,
807 backoff_strategy: "exponential".to_string(),
808 initial_delay_ms: 100,
809 max_delay_ms: 60000,
810 },
811 }]),
812 custom_scalars: None,
813 observers_config: None,
814 federation_config: None,
815 };
816
817 let report = SchemaValidator::validate(&schema).unwrap();
818 assert!(!report.is_valid());
819 assert!(report.errors.iter().any(|e| e.message.contains("invalid event")));
820 }
821
822 #[test]
823 fn test_observer_with_invalid_action_type() {
824 use serde_json::json;
825
826 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
827
828 let schema = IntermediateSchema {
829 security: None,
830 version: "2.0.0".to_string(),
831 types: vec![IntermediateType {
832 name: "Order".to_string(),
833 fields: vec![],
834 description: None,
835 implements: vec![],
836 is_error: false,
837 }],
838 enums: vec![],
839 input_types: vec![],
840 interfaces: vec![],
841 unions: vec![],
842 queries: vec![],
843 mutations: vec![],
844 subscriptions: vec![],
845 fragments: None,
846 directives: None,
847 fact_tables: None,
848 aggregate_queries: None,
849 observers: Some(vec![IntermediateObserver {
850 name: "onOrderCreated".to_string(),
851 entity: "Order".to_string(),
852 event: "INSERT".to_string(),
853 actions: vec![json!({"type": "invalid_action"})],
854 condition: None,
855 retry: IntermediateRetryConfig {
856 max_attempts: 3,
857 backoff_strategy: "exponential".to_string(),
858 initial_delay_ms: 100,
859 max_delay_ms: 60000,
860 },
861 }]),
862 custom_scalars: None,
863 observers_config: None,
864 federation_config: None,
865 };
866
867 let report = SchemaValidator::validate(&schema).unwrap();
868 assert!(!report.is_valid());
869 assert!(report.errors.iter().any(|e| e.message.contains("invalid type")));
870 }
871
872 #[test]
873 fn test_observer_with_invalid_retry_config() {
874 use serde_json::json;
875
876 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
877
878 let schema = IntermediateSchema {
879 security: None,
880 version: "2.0.0".to_string(),
881 types: vec![IntermediateType {
882 name: "Order".to_string(),
883 fields: vec![],
884 description: None,
885 implements: vec![],
886 is_error: false,
887 }],
888 enums: vec![],
889 input_types: vec![],
890 interfaces: vec![],
891 unions: vec![],
892 queries: vec![],
893 mutations: vec![],
894 subscriptions: vec![],
895 fragments: None,
896 directives: None,
897 fact_tables: None,
898 aggregate_queries: None,
899 observers: Some(vec![IntermediateObserver {
900 name: "onOrderCreated".to_string(),
901 entity: "Order".to_string(),
902 event: "INSERT".to_string(),
903 actions: vec![json!({"type": "webhook", "url": "https://example.com"})],
904 condition: None,
905 retry: IntermediateRetryConfig {
906 max_attempts: 3,
907 backoff_strategy: "invalid_strategy".to_string(),
908 initial_delay_ms: 100,
909 max_delay_ms: 60000,
910 },
911 }]),
912 custom_scalars: None,
913 observers_config: None,
914 federation_config: None,
915 };
916
917 let report = SchemaValidator::validate(&schema).unwrap();
918 assert!(!report.is_valid());
919 assert!(report.errors.iter().any(|e| e.message.contains("invalid backoff_strategy")));
920 }
921}