1use std::collections::{BTreeMap, HashMap};
2use std::path::PathBuf;
3
4use convex::Value as ConvexValue;
5use oxc::allocator::Allocator;
6use oxc::diagnostics::OxcDiagnostic;
7use oxc::parser::Parser;
8use oxc::semantic::SemanticBuilder;
9use oxc::span::SourceType;
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value as JsonValue};
12
13use crate::errors::ConvexTypeGeneratorError;
14
15#[derive(Debug, Serialize, Deserialize)]
19pub(crate) struct ConvexSchema
20{
21 pub(crate) tables: Vec<ConvexTable>,
22}
23
24#[derive(Debug, Serialize, Deserialize)]
28pub(crate) struct ConvexTable
29{
30 pub(crate) name: String,
32 pub(crate) columns: Vec<ConvexColumn>,
34}
35
36#[derive(Debug, Serialize, Deserialize)]
38pub(crate) struct ConvexColumn
39{
40 pub(crate) name: String,
42 pub(crate) data_type: JsonValue,
45}
46
47pub(crate) type ConvexFunctions = Vec<ConvexFunction>;
49
50#[derive(Debug, Serialize, Deserialize)]
54pub(crate) struct ConvexFunction
55{
56 pub(crate) name: String,
57 pub(crate) params: Vec<ConvexFunctionParam>,
58 pub(crate) type_: String,
59 pub(crate) file_name: String,
60}
61
62#[derive(Debug, Serialize, Deserialize)]
64pub(crate) struct ConvexFunctionParam
65{
66 pub(crate) name: String,
67 pub(crate) data_type: JsonValue,
68}
69
70pub(crate) fn create_schema_ast(path: PathBuf) -> Result<JsonValue, ConvexTypeGeneratorError>
81{
82 if !path.exists() {
84 return Err(ConvexTypeGeneratorError::MissingSchemaFile);
85 }
86
87 generate_ast(&path)
88}
89
90pub(crate) fn create_functions_ast(paths: Vec<PathBuf>) -> Result<HashMap<String, JsonValue>, ConvexTypeGeneratorError>
92{
93 let mut functions = HashMap::new();
94
95 for path in paths {
96 let function_ast = generate_ast(&path)?;
97 let path_str = path.to_string_lossy().to_string();
98 let file_name = path
99 .file_name()
100 .ok_or_else(|| ConvexTypeGeneratorError::InvalidPath(path_str.clone()))?
101 .to_str()
102 .ok_or(ConvexTypeGeneratorError::InvalidUnicode(path_str))?;
103
104 functions.insert(file_name.to_string(), function_ast);
105 }
106
107 Ok(functions)
108}
109
110pub(crate) fn parse_schema_ast(ast: JsonValue) -> Result<ConvexSchema, ConvexTypeGeneratorError>
111{
112 let context = "root";
113 let body = ast["body"]
115 .as_array()
116 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
117 context: context.to_string(),
118 details: "Missing body array".to_string(),
119 })?;
120
121 let define_schema = find_define_schema(body).ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
123 context: context.to_string(),
124 details: "Could not find defineSchema call".to_string(),
125 })?;
126
127 let schema_args = define_schema["arguments"]
129 .as_array()
130 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
131 context: context.to_string(),
132 details: "Missing schema arguments".to_string(),
133 })?;
134
135 let tables_obj = schema_args
137 .first()
138 .and_then(|arg| arg["properties"].as_array())
139 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
140 context: context.to_string(),
141 details: "Missing table definitions".to_string(),
142 })?;
143
144 let mut tables = Vec::new();
145
146 for table_prop in tables_obj {
148 let table_name = table_prop["key"]["name"]
150 .as_str()
151 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
152 context: context.to_string(),
153 details: "Invalid table name".to_string(),
154 })?;
155
156 let define_table_args =
158 table_prop["value"]["arguments"]
159 .as_array()
160 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
161 context: context.to_string(),
162 details: "Invalid table definition".to_string(),
163 })?;
164
165 let columns_obj = define_table_args
167 .first()
168 .and_then(|arg| arg["properties"].as_array())
169 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
170 context: context.to_string(),
171 details: "Missing column definitions".to_string(),
172 })?;
173
174 let mut columns = Vec::new();
175
176 for column_prop in columns_obj {
178 let column_name =
180 column_prop["key"]["name"]
181 .as_str()
182 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
183 context: context.to_string(),
184 details: "Invalid column name".to_string(),
185 })?;
186
187 let mut context = TypeContext::new(context.to_string());
189 let column_type = extract_column_type(column_prop, &mut context)?;
190
191 columns.push(ConvexColumn {
192 name: column_name.to_string(),
193 data_type: column_type,
194 });
195 }
196
197 tables.push(ConvexTable {
198 name: table_name.to_string(),
199 columns,
200 });
201 }
202
203 Ok(ConvexSchema { tables })
204}
205
206fn find_define_schema(body: &[JsonValue]) -> Option<&JsonValue>
208{
209 for node in body {
210 if let Some(declaration) = node.get("declaration") {
212 if declaration["type"].as_str() == Some("CallExpression") {
214 if let Some(callee) = declaration.get("callee") {
216 if callee["type"].as_str() == Some("Identifier") && callee["name"].as_str() == Some("defineSchema") {
217 return Some(declaration);
218 }
219 }
220 }
221 }
222
223 if node["type"].as_str() == Some("CallExpression") {
226 if let Some(callee) = node.get("callee") {
227 if callee["type"].as_str() == Some("Identifier") && callee["name"].as_str() == Some("defineSchema") {
228 return Some(node);
229 }
230 }
231 }
232 }
233 None
234}
235
236fn extract_column_type(column_prop: &JsonValue, context: &mut TypeContext) -> Result<JsonValue, ConvexTypeGeneratorError>
238{
239 let value = &column_prop["value"];
240 let callee = &value["callee"];
241
242 let type_name = callee["property"]["name"]
243 .as_str()
244 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
245 context: context.get_error_context(),
246 details: "Invalid column type".to_string(),
247 })?;
248
249 validate_type_name(type_name)?;
251
252 let binding = Vec::new();
253 let args = value["arguments"].as_array().unwrap_or(&binding);
254
255 let mut type_obj = serde_json::Map::new();
256 type_obj.insert("type".to_string(), JsonValue::String(type_name.to_string()));
257
258 match type_name {
260 "optional" => {
261 if let Some(inner_type) = args.first() {
263 let inner_type_prop = json!({
264 "key": { "name": "inner" },
265 "value": inner_type
266 });
267 context.type_path.push("inner".to_string());
268 let parsed_inner_type = extract_column_type(&inner_type_prop, context)?;
269 context.type_path.pop();
270 type_obj.insert("inner".to_string(), parsed_inner_type);
271 } else {
272 return Err(ConvexTypeGeneratorError::InvalidSchema {
273 context: context.type_path.join("."),
274 details: "Optional type must have an inner type".to_string(),
275 });
276 }
277 }
278 "array" => {
279 if let Some(element_type) = args.first() {
281 let element_type_prop = json!({
282 "key": { "name": "element" },
283 "value": element_type
284 });
285 context.type_path.push("elements".to_string());
286 let parsed_element_type = extract_column_type(&element_type_prop, context)?;
287 context.type_path.pop();
288 type_obj.insert("elements".to_string(), parsed_element_type);
289 }
290 }
291 "object" => {
292 if let Some(obj_def) = args.first() {
294 if let Some(properties) = obj_def["properties"].as_array() {
295 let mut prop_types = serde_json::Map::new();
296
297 for prop in properties {
298 let prop_name =
299 prop["key"]["name"]
300 .as_str()
301 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
302 context: context.type_path.join("."),
303 details: "Invalid object property name".to_string(),
304 })?;
305
306 let prop_type = extract_column_type(prop, context)?;
307 prop_types.insert(prop_name.to_string(), prop_type);
308 }
309
310 type_obj.insert("properties".to_string(), JsonValue::Object(prop_types));
311 }
312 }
313 }
314 "record" => {
315 if args.len() >= 2 {
317 let key_type_prop = json!({
319 "key": { "name": "key" },
320 "value": args[0]
321 });
322 let key_type = extract_column_type(&key_type_prop, context)?;
323 type_obj.insert("keyType".to_string(), key_type);
324
325 let value_type_prop = json!({
327 "key": { "name": "value" },
328 "value": args[1]
329 });
330 let value_type = extract_column_type(&value_type_prop, context)?;
331 type_obj.insert("valueType".to_string(), value_type);
332 }
333 }
334 "union" => {
335 let mut variants = Vec::new();
337 for variant in args {
338 let variant_prop = json!({
339 "key": { "name": "variant" },
340 "value": variant
341 });
342 let variant_type = extract_column_type(&variant_prop, context)?;
343 variants.push(variant_type);
344 }
345 type_obj.insert("variants".to_string(), JsonValue::Array(variants));
346 }
347 "literal" => {
348 if let Some(literal_value) = args.first() {
350 type_obj.insert("value".to_string(), literal_value.clone());
351 }
352 }
353 _ => {
355 if !args.is_empty() {
356 type_obj.insert("arguments".to_string(), JsonValue::Array(args.to_vec()));
357 }
358 }
359 }
360
361 let type_value = JsonValue::Object(type_obj);
363
364 check_circular_references(&type_value, context)?;
366
367 Ok(type_value)
368}
369
370pub(crate) fn parse_function_ast(ast_map: HashMap<String, JsonValue>) -> Result<ConvexFunctions, ConvexTypeGeneratorError>
371{
372 let mut functions = Vec::new();
373
374 for (file_name, ast) in ast_map {
375 let file_name = file_name.strip_suffix(".ts").unwrap_or(&file_name).to_string();
377
378 let body = ast["body"]
380 .as_array()
381 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
382 context: format!("file_{}", file_name),
383 details: "Missing body array".to_string(),
384 })?;
385
386 for node in body {
387 if node["type"].as_str() == Some("ExportNamedDeclaration") {
389 if let Some(declaration) = node.get("declaration") {
390 if declaration["type"].as_str() == Some("VariableDeclaration") {
392 if let Some(declarators) = declaration["declarations"].as_array() {
393 for declarator in declarators {
394 let name = declarator["id"]["name"].as_str().ok_or_else(|| {
396 ConvexTypeGeneratorError::InvalidSchema {
397 context: format!("file_{}", file_name),
398 details: "Missing function name".to_string(),
399 }
400 })?;
401
402 let init = &declarator["init"];
404 if init["type"].as_str() == Some("CallExpression") {
405 let fn_type = init["callee"]["name"].as_str().ok_or_else(|| {
407 ConvexTypeGeneratorError::InvalidSchema {
408 context: format!("function_{}", name),
409 details: "Missing function type".to_string(),
410 }
411 })?;
412
413 if let Some(args) = init["arguments"].as_array() {
415 if let Some(config) = args.first() {
416 let params = extract_function_params(config, &file_name)?;
418
419 functions.push(ConvexFunction {
420 name: name.to_string(),
421 params,
422 type_: fn_type.to_string(),
423 file_name: file_name.to_string(),
424 });
425 }
426 }
427 }
428 }
429 }
430 }
431 }
432 }
433 }
434 }
435
436 Ok(functions)
437}
438
439fn extract_function_params(config: &JsonValue, file_name: &str)
441 -> Result<Vec<ConvexFunctionParam>, ConvexTypeGeneratorError>
442{
443 let mut params = Vec::new();
444
445 if let Some(properties) = config["properties"].as_array() {
447 for prop in properties {
448 if prop["key"]["name"].as_str() == Some("args") {
449 if prop["value"]["type"].as_str() != Some("ObjectExpression") {
451 return Err(ConvexTypeGeneratorError::InvalidSchema {
452 context: format!("file_{}", file_name),
453 details: "Function args must be an object".to_string(),
454 });
455 }
456
457 if let Some(args_props) = prop["value"]["properties"].as_array() {
459 for arg_prop in args_props {
460 if !arg_prop["type"].as_str().map_or(false, |t| t == "ObjectProperty") {
462 return Err(ConvexTypeGeneratorError::InvalidSchema {
463 context: format!("file_{}", file_name),
464 details: "Invalid argument property structure".to_string(),
465 });
466 }
467
468 let param_name =
470 arg_prop["key"]["name"]
471 .as_str()
472 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
473 context: format!("file_{}", file_name),
474 details: "Invalid parameter name".to_string(),
475 })?;
476
477 let mut context = TypeContext::new(format!("function_{}", param_name));
479 let param_type = extract_column_type(arg_prop, &mut context)?;
480
481 params.push(ConvexFunctionParam {
482 name: param_name.to_string(),
483 data_type: param_type,
484 });
485 }
486 }
487 break; }
489 }
490 }
491
492 Ok(params)
493}
494
495fn generate_ast(path: &PathBuf) -> Result<JsonValue, ConvexTypeGeneratorError>
503{
504 let path_str = path.to_string_lossy().to_string();
505 let allocator = Allocator::default();
506
507 let source_text = std::fs::read_to_string(path).map_err(|error| ConvexTypeGeneratorError::IOError {
509 file: path_str.clone(),
510 error,
511 })?;
512
513 if source_text.trim().is_empty() {
514 return Err(ConvexTypeGeneratorError::EmptySchemaFile { file: path_str });
515 }
516
517 let source_type = SourceType::from_path(path).map_err(|_| ConvexTypeGeneratorError::ParsingFailed {
518 file: path_str.clone(),
519 details: "Failed to determine source type".to_string(),
520 })?;
521
522 let mut errors: Vec<OxcDiagnostic> = Vec::new();
523
524 let ret = Parser::new(&allocator, &source_text, source_type).parse();
525 errors.extend(ret.errors);
526
527 if ret.panicked {
528 for error in &errors {
529 eprintln!("{error:?}");
530 }
531 return Err(ConvexTypeGeneratorError::ParsingFailed {
532 file: path_str.clone(),
533 details: "Parser panicked".to_string(),
534 });
535 }
536
537 if ret.program.is_empty() {
538 return Err(ConvexTypeGeneratorError::EmptySchemaFile { file: path_str });
539 }
540
541 let semantics = SemanticBuilder::new().with_check_syntax_error(true).build(&ret.program);
542 errors.extend(semantics.errors);
543
544 if !errors.is_empty() {
545 for error in &errors {
546 eprintln!("{error:?}");
547 }
548 return Err(ConvexTypeGeneratorError::ParsingFailed {
549 file: path_str,
550 details: "Semantic analysis failed".to_string(),
551 });
552 }
553
554 serde_json::to_value(&ret.program).map_err(ConvexTypeGeneratorError::SerializationFailed)
555}
556
557const VALID_CONVEX_TYPES: &[&str] = &[
558 "id", "null", "int64", "number", "boolean", "string", "bytes", "array", "object", "record", "union", "literal",
559 "optional", "any",
560];
561
562fn validate_type_name(type_name: &str) -> Result<(), ConvexTypeGeneratorError>
563{
564 if !VALID_CONVEX_TYPES.contains(&type_name) {
565 return Err(ConvexTypeGeneratorError::InvalidType {
566 found: type_name.to_string(),
567 valid_types: VALID_CONVEX_TYPES.iter().map(|&s| s.to_string()).collect(),
568 });
569 }
570 Ok(())
571}
572
573#[derive(Debug, Default)]
574struct TypeContext
575{
576 type_stack: Vec<(String, String)>, file_name: String,
580 type_path: Vec<String>,
582}
583
584impl TypeContext
585{
586 fn new(file_name: String) -> Self
587 {
588 Self {
589 file_name,
590 type_stack: Vec::new(),
591 type_path: Vec::new(),
592 }
593 }
594
595 fn push_type(&mut self, type_name: &str) -> Result<(), ConvexTypeGeneratorError>
596 {
597 let current_path = self.type_path.join(".");
598
599 if type_name == "object" {
602 let full_path = if current_path.is_empty() {
603 type_name.to_string()
604 } else {
605 format!("{}.{}", current_path, type_name)
606 };
607
608 if self.type_stack.iter().any(|(_, path)| path == &full_path) {
610 return Err(ConvexTypeGeneratorError::CircularReference {
611 path: self.type_stack.iter().map(|(_, path)| path.clone()).collect(),
612 });
613 }
614 self.type_stack.push((type_name.to_string(), full_path));
615 }
616 Ok(())
617 }
618
619 fn get_error_context(&self) -> String
621 {
622 format!("{}:{}", self.file_name, self.type_path.join("."))
623 }
624
625 fn pop_type(&mut self)
627 {
628 if let Some((type_name, _)) = self.type_stack.last() {
630 if type_name == "object" {
631 self.type_stack.pop();
632 }
633 }
634 }
635}
636
637fn check_circular_references(type_obj: &JsonValue, context: &mut TypeContext) -> Result<(), ConvexTypeGeneratorError>
638{
639 let type_name = type_obj["type"]
640 .as_str()
641 .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
642 context: context.type_path.join("."),
643 details: "Missing type name".to_string(),
644 })?;
645
646 context.push_type(type_name)?;
647
648 match type_name {
649 "optional" => {
650 if let Some(inner) = type_obj.get("inner") {
651 context.type_path.push("inner".to_string());
652 check_circular_references(inner, context)?;
653 context.type_path.pop();
654 }
655 }
656 "array" => {
657 if let Some(elements) = type_obj.get("elements") {
658 context.type_path.push("elements".to_string());
659 check_circular_references(elements, context)?;
660 context.type_path.pop();
661 }
662 }
663 "object" => {
664 if let Some(properties) = type_obj.get("properties") {
665 if let Some(props) = properties.as_object() {
666 for (prop_name, prop_type) in props {
667 context.type_path.push(prop_name.to_string());
668 check_circular_references(prop_type, context)?;
669 context.type_path.pop();
670 }
671 }
672 }
673 }
674 "record" => {
675 if let Some(key_type) = type_obj.get("keyType") {
677 context.type_path.push("keyType".to_string());
678 check_circular_references(key_type, context)?;
679 context.type_path.pop();
680 }
681 if let Some(value_type) = type_obj.get("valueType") {
683 context.type_path.push("valueType".to_string());
684 check_circular_references(value_type, context)?;
685 context.type_path.pop();
686 }
687 }
688 "union" | "intersection" => {
689 if let Some(variants) = type_obj["variants"].as_array() {
690 for (i, variant) in variants.iter().enumerate() {
691 context.type_path.push(format!("variant_{}", i));
692 check_circular_references(variant, context)?;
693 context.type_path.pop();
694 }
695 }
696 }
697 _ => {} }
699
700 context.pop_type();
701 Ok(())
702}
703
704pub trait IntoConvexValue
706{
707 fn into_convex_value(self) -> ConvexValue;
709}
710
711impl IntoConvexValue for JsonValue
712{
713 fn into_convex_value(self) -> ConvexValue
714 {
715 match self {
716 JsonValue::Null => ConvexValue::Null,
717 JsonValue::Bool(b) => ConvexValue::Boolean(b),
718 JsonValue::Number(n) => {
719 if let Some(i) = n.as_i64() {
720 ConvexValue::Int64(i)
721 } else if let Some(f) = n.as_f64() {
722 ConvexValue::Float64(f)
723 } else {
724 ConvexValue::Null
725 }
726 }
727 JsonValue::String(s) => ConvexValue::String(s),
728 JsonValue::Array(arr) => ConvexValue::Array(arr.into_iter().map(|v| v.into_convex_value()).collect()),
729 JsonValue::Object(map) => {
730 let converted: BTreeMap<String, ConvexValue> =
731 map.into_iter().map(|(k, v)| (k, v.into_convex_value())).collect();
732 ConvexValue::Object(converted)
733 }
734 }
735 }
736}
737
738pub trait ConvexValueExt
739{
740 fn into_serde_value(self) -> JsonValue;
742}
743
744impl ConvexValueExt for ConvexValue
745{
746 fn into_serde_value(self) -> JsonValue
747 {
748 match self {
749 ConvexValue::Null => JsonValue::Null,
750 ConvexValue::Boolean(b) => JsonValue::Bool(b),
751 ConvexValue::Int64(i) => JsonValue::Number(i.into()),
752 ConvexValue::Float64(f) => {
753 if let Some(n) = serde_json::Number::from_f64(f) {
754 JsonValue::Number(n)
755 } else {
756 JsonValue::Null
757 }
758 }
759 ConvexValue::String(s) => JsonValue::String(s),
760 ConvexValue::Array(arr) => JsonValue::Array(arr.into_iter().map(|v| v.into_serde_value()).collect()),
761 ConvexValue::Object(map) => JsonValue::Object(map.into_iter().map(|(k, v)| (k, v.into_serde_value())).collect()),
762 ConvexValue::Bytes(b) => JsonValue::Array(b.into_iter().map(|byte| JsonValue::Number(byte.into())).collect()),
763 }
764 }
765}
766
767pub trait ConvexClientExt
769{
770 fn prepare_args<T: Into<BTreeMap<String, JsonValue>>>(args: T) -> BTreeMap<String, ConvexValue>
772 {
773 args.into().into_iter().map(|(k, v)| (k, v.into_convex_value())).collect()
774 }
775}
776
777impl ConvexClientExt for convex::ConvexClient {}