packages feed

convex-schema-parser 0.1.3.0 → 0.1.4.0

raw patch · 9 files changed

+700/−71 lines, 9 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Convex.Parser: parseProjectFromContents :: String -> String -> [(String, String)] -> IO (Either String ParsedProject)
+ Convex.Parser: runUnificationPass :: ParsedProject -> ParsedProject

Files

CHANGELOG.md view
@@ -1,5 +1,52 @@ # Revision history for convex-schema-parser +## 0.1.4.0 -- 2025-07-09++*   **Fixes:**+    *   Properly unify nested types during the unification pass.++## 0.1.3.0 -- 2025-07-08++*   **Schema Parsing:**+    *   More robust implementation of the schema parser provides better error handling and more accurate parsing.+    *   Correctly handles foreign imported types.+    *   Support for `i64`, `f64`, `ArrayBuffer`, and `v.bytes()` has been added.++*   **Backend Improvements:**+    *   **Rust:**+        *   Custom `serde` implementation for `convex::Value`.+        *   Validator for generated types.+        *   Typed subscription streams for Convex subscriptions.+    *   **Python:**+        *   Validator for generated types.+        *   Pydantic annotations to handle fields prefixed with `_`.+        *   Typed subscription generators for Convex subscriptions.++*   **Developer Experience:**+    *   New `init` command to guide users through the setup process.+    *   `optparse-applicative` for a better CLI experience.+    *   New "dev mode" to automatically regenerate code on file changes.+    *   `justfile` to provide a simple way to run common commands.++*   **Fixes:**+    *   **Rust Backend:**+        *   Correctly use the inner optional value when generating functions.+        *   Fixed a bug where `innerValueConversion` was not being properly handled for functions.+        *   Custom structs now know how to encode themselves to `convex::Value`.+    *   **Python Backend:**+        *   Removed redundant imports from the generated Python code.+        *   Properly indent code with helpers.+        *   Reverted to a sync client.+    *   **General:**+        *   Normalized generated strings for easier testing.+        *   Namespaced functions to avoid conflicts in the Python and Rust backends.+        *   Fixed a bug where empty function arguments were not being properly handled in the action parser.++*   **Miscellaneous:**+    *   Updated README and LICENSE.+    *   Added a CI/CD pipeline for releases.+    *   Support for GHC 9.6.7.+ ## 0.1.0 -- 2025-06-30  * First version
README.md view
@@ -14,6 +14,10 @@ ## Installation  The easiest way to use `convex-schema-parser` is currently through the Cabal package manager.++> [!NOTE]+> We are on hackage now! So you can simply run `cabal update` (important, this package is a recent addition to Hackage) and then `cabal install convex-schema-parser` to install the tool quickly without cloning.+ We provide prebuilt binaries for `linux` & `macOS` that you can download and run directly, but `macOS` users have to allow the binary to run first since we do not sign it (yet). A `npm` package `@parsonosai/convex-schema-parser` is also on its way and supported as soon as we get code-signing ready, we currently use a placeholder. 
convex-schema-parser.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.0 name:            convex-schema-parser-version:         0.1.3.0+version:         0.1.4.0 license:         MIT author:          Norbert Dzikowski maintainer:      lambdax.one@icloud.com@@ -26,6 +26,24 @@ common warnings     ghc-options: -Wall +-- library convex-client+--     import:           warnings+--     exposed-modules:  Convex.Client.Client+--         , Convex.Client.Types+--     build-depends:    base >= 4.18.3 && < 4.21+--         , websockets >= 0.12.7 && < 0.13+--         , wuss >= 1.1.1 && < 1.2+--         , async >= 2.2.4 && < 2.3+--         , aeson >= 2.2.3 && < 2.3+--         , stm >= 2.5.3 && < 2.6+--         , text >= 2.1 && < 2.2+--         , uuid >= 1.3.13 && < 1.4+--         , data-default >= 0.7.1 && < 0.8+--         , mtl >= 2.3.1 && < 2.4+--         , containers >= 0.7 && < 0.8+--     hs-source-dirs:   src+--     default-language: Haskell2010+ library convex-schema-parser-lib     import:           warnings     exposed-modules:  Convex.Schema.Parser@@ -75,6 +93,7 @@         , ApiParserTest         , SchemaParserTest         , RustSerializationTest+        , UnificationTest     hs-source-dirs:      test     build-depends:       base,                          HUnit >=1.6.0 && < 1.7,
src/Backend/Python.hs view
@@ -243,6 +243,7 @@         Schema.VObject _ -> True         Schema.VArray (Schema.VObject _) -> True         Schema.VReference _ -> True+        Schema.VArray (Schema.VReference _) -> True         _ -> False    in (pyType, isModel, nested) 
src/Backend/Rust.hs view
@@ -452,6 +452,13 @@ generateTableStruct table =   let className = toPascalCase (Schema.tableName table) ++ "Doc"       (fieldLines, nestedFromFields) = unzip $ map (generateField className) (Schema.tableFields table)+      allFields =+        [ ("_id", Schema.VId (toPascalCase (Schema.tableName table))),+          ("_creation_time", Schema.VFloat64)+        ]+          ++ map (\f -> (Schema.fieldName f, Schema.fieldType f)) (Schema.tableFields table)+      fromBlock = generateFromConvexValueImpl (className) allFields+      toBlock = generateToConvexValueImpl (className) allFields    in ( unlines           [ "#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)]",             ("pub struct " ++ className ++ " {"),@@ -459,9 +466,13 @@             indent 1 ("pub _id: Id<" ++ className ++ ">,"),             indent 1 "#[serde(default)]",             indent 1 "#[serde(rename = \"_creationTime\")]",-            indent 1 "pub creation_time: f64,",+            indent 1 "pub _creation_time: f64,",             unlines fieldLines,-            "}"+            "}",+            "",+            fromBlock,+            "",+            toBlock           ],         concat nestedFromFields       )@@ -723,7 +734,7 @@       getterName = "get_" ++ fieldNameSnake       (fieldTypeStr, _) = case stripPrefix (reverse "Object") (reverse structName) of         Just cleanStructName -> toRustType (reverse cleanStructName ++ capitalize fieldName) fieldType-        Nothing -> error "Expected struct name to end with 'Object' for optional field"+        Nothing -> toRustType (structName ++ capitalize fieldName) fieldType    in unlines         [ indent 0 $ "fn " ++ getterName ++ "(map: &BTreeMap<String, Value>, key: &str) -> Result<Option<" ++ fieldTypeStr ++ ">, ApiError> {",           indent 1 $ "match map.get(key) {",@@ -740,7 +751,7 @@       getterName = "get_" ++ fieldNameSnake       (fieldTypeStr, _) = case stripPrefix (reverse "Object") (reverse structName) of         Just cleanStructName -> toRustType (reverse cleanStructName ++ capitalize fieldName) fieldType-        Nothing -> error "Expected struct name to end with 'Object' for optional field"+        Nothing -> toRustType (structName ++ capitalize fieldName) fieldType    in unlines         [ indent 0 $ "fn " ++ getterName ++ "(map: &BTreeMap<String, Value>, key: &str) -> Result<" ++ fieldTypeStr ++ ", ApiError> {",           indent 1 $ "match map.get(key) {",
src/Convex/Parser.hs view
@@ -1,11 +1,13 @@+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -module Convex.Parser (parseProject, ParsedProject (..), apiFileParser) where+module Convex.Parser (parseProject, parseProjectFromContents, ParsedProject (..), apiFileParser, runUnificationPass) where  import Control.Monad (forM, void) import qualified Convex.Action.Parser as Action import qualified Convex.Schema.Parser as Schema-import Data.List (sort)+import Data.Char (toUpper)+import Data.List (sort, sortOn) import qualified Data.Map as Map import System.FilePath (replaceExtension, (</>)) import Text.Parsec@@ -53,44 +55,45 @@       void $ apiIdentifier -- Consume the alias, we don't need it       return path -parseProject :: FilePath -> FilePath -> IO (Either String ParsedProject)-parseProject schemaPath declRootDir = do-  -- Parse the source schema file first to get tables and the initial state with constants.-  schemaContent <- readFile schemaPath+parseProjectFromContents ::+  String ->+  String ->+  [(String, String)] ->+  IO (Either String ParsedProject)+parseProjectFromContents schemaContent apiFileContent actionContents = do+  -- 1. Parse Schema   schemaResult <- Schema.parseSchema schemaContent-   case schemaResult of     Left err -> return $ Left ("Failed to parse schema.ts: " ++ show err)     Right schemaFile -> do-      -- Re-construct the initial state for the action parser from the parsed constants.       let initialState = Schema.ParserState {Schema.psConstants = Schema.parsedConstants schemaFile} -      -- Parse the _generated/api.d.ts file to discover function modules.-      let apiFilePath = declRootDir </> "_generated" </> "api.d.ts"-      apiFileContent <- readFile apiFilePath-      let modulePaths = case parse apiFileParser apiFilePath apiFileContent of-            Left _ -> []+      -- Parse API file to get module paths+      let modulePaths = case parse apiFileParser "(api.d.ts)" apiFileContent of+            Left _ -> [] -- Or should this be an error? The original returns []             Right paths -> filter (/= "schema") paths -      putStrLn $ "Found " ++ show (length modulePaths) ++ " action modules in: " ++ apiFilePath+      let actionContentMap = Map.fromList actionContents -      -- For each discovered module, parse its corresponding .d.ts file.-      allFunctions <- fmap concat $ forM modulePaths $ \modulePath -> do-        let fullPath = declRootDir </> replaceExtension modulePath ".d.ts"+      -- Parse action files+      allFunctions <- fmap concat . forM modulePaths $ \modulePath -> do         let astPath = replaceExtension modulePath ""--        actionContent <- readFile fullPath-        actionResult <- runParserT (Action.parseActionFile astPath) initialState fullPath actionContent--        case actionResult of-          Left err -> do-            putStrLn $ "Failed to parse actions from: " ++ fullPath ++ " | Error: " ++ show err+        case Map.lookup modulePath actionContentMap of+          Nothing -> do+            putStrLn $ "Action content not found for module: " ++ modulePath             return []-          Right funcs -> do-            putStrLn $ "Parsed actions from: " ++ fullPath-            putStrLn $ show (length funcs) ++ " functions found"-            return funcs+          Just actionContent -> do+            actionResult <- runParserT (Action.parseActionFile astPath) initialState modulePath actionContent+            case actionResult of+              Left err -> do+                putStrLn $ "Failed to parse actions from: " ++ modulePath ++ " | Error: " ++ show err+                return []+              Right funcs -> do+                putStrLn $ "Parsed actions from: " ++ modulePath+                putStrLn $ show (length funcs) ++ " functions found"+                return funcs +      -- Construct and unify project       let project =             ParsedProject               { ppSchema = Schema.parsedSchema schemaFile,@@ -98,49 +101,152 @@                 ppFunctions = allFunctions               } -      return $ Right . unifyProjectTypes $ project+      return . Right . runUnificationPass $ project +parseProject :: FilePath -> FilePath -> IO (Either String ParsedProject)+parseProject schemaPath declRootDir = do+  -- Parse the source schema file first to get tables and the initial state with constants.+  schemaContent <- readFile schemaPath+  -- Parse the _generated/api.d.ts file to discover function modules.+  let apiFilePath = declRootDir </> "_generated" </> "api.d.ts"+  apiFileContent <- readFile apiFilePath+  let modulePaths = case parse apiFileParser apiFilePath apiFileContent of+        Left _ -> []+        Right paths -> filter (/= "schema") paths++  putStrLn $ "Found " ++ show (length modulePaths) ++ " action modules in: " ++ apiFilePath++  -- For each discovered module, parse its corresponding .d.ts file.+  actionContents <- forM modulePaths $ \modulePath -> do+    let fullPath = declRootDir </> replaceExtension modulePath ".d.ts"+    content <- readFile fullPath+    return (modulePath, content)++  parseProjectFromContents schemaContent apiFileContent actionContents+ type UnionSignatureMap = Map.Map [String] String --- | Pre-processes the parsed project to replace anonymous unions with named references--- if they structurally match.-unifyProjectTypes :: ParsedProject -> ParsedProject-unifyProjectTypes project =-  let unionMap = buildUnionSignatureMap (ppConstants project)-   in project {ppFunctions = map (unifyFunctionTypes unionMap) (ppFunctions project)}+type ObjectSignatureMap = Map.Map [(String, Schema.ConvexType)] String++-- | Pre-processes the parsed project to replace anonymous unions and objects+-- with named references if they structurally match. This is done iteratively+-- to a fixed point to handle nested structures.+runUnificationPass :: ParsedProject -> ParsedProject+runUnificationPass project =+  let ephemeralProject = addTableDocsToConstants project+      unifiedProject = go ephemeralProject+   in unifiedProject {ppConstants = ppConstants project} -- Discard ephemeral constants   where-    buildUnionSignatureMap :: Map.Map String Schema.ConvexType -> UnionSignatureMap-    buildUnionSignatureMap constants =-      Map.fromList-        [ (sort $ map Schema.getLiteralString literals, name)-        | (name, Schema.VUnion literals) <- Map.toList constants,-          all Schema.isLiteral literals-        ]+    go currentProject =+      let nextProject = unifyOnce currentProject+       in if nextProject == currentProject+            then currentProject+            else go nextProject -    unifyFunctionTypes :: UnionSignatureMap -> Action.ConvexFunction -> Action.ConvexFunction-    unifyFunctionTypes unionMap func =-      func-        { Action.funcArgs = map (unifyArgType unionMap) (Action.funcArgs func),-          Action.funcReturn = unifyTypeRecursively unionMap (Action.funcReturn func)-        }+canonicalizeType :: Schema.ConvexType -> Schema.ConvexType+canonicalizeType (Schema.VObject fields) =+  Schema.VObject . sortOn fst . map (\(n, t) -> (n, canonicalizeType t)) $ fields+canonicalizeType (Schema.VUnion types) =+  Schema.VUnion . sort . map canonicalizeType $ types+canonicalizeType (Schema.VArray t) = Schema.VArray (canonicalizeType t)+canonicalizeType (Schema.VOptional t) = Schema.VOptional (canonicalizeType t)+canonicalizeType other = other -    unifyArgType :: UnionSignatureMap -> (String, Schema.ConvexType) -> (String, Schema.ConvexType)-    unifyArgType unionMap (argName, argType) =-      (argName, unifyTypeRecursively unionMap argType)+addTableDocsToConstants :: ParsedProject -> ParsedProject+addTableDocsToConstants project =+  let tableDocs =+        Map.fromList+          [ ( toPascalCase (Schema.tableName table) ++ "Doc",+              Schema.VObject . sortOn fst $+                [("_id", Schema.VId (Schema.tableName table)), ("_creationTime", Schema.VNumber)]+                  ++ map+                    (\f -> (Schema.fieldName f, Schema.fieldType f))+                    (Schema.tableFields table)+            )+          | table <- Schema.getTables (ppSchema project)+          ]+      allConstants = Map.union (ppConstants project) tableDocs+   in project {ppConstants = allConstants} -    -- This new recursive function traverses the entire type structure.-    unifyTypeRecursively :: UnionSignatureMap -> Schema.ConvexType -> Schema.ConvexType-    unifyTypeRecursively unionMap u@(Schema.VUnion literals)+toPascalCase :: String -> String+toPascalCase [] = []+toPascalCase (h : t) = toUpper h : t++buildUnionSignatureMap :: Map.Map String Schema.ConvexType -> UnionSignatureMap+buildUnionSignatureMap constants =+  Map.fromList+    [ (sort $ map Schema.getLiteralString literals, name)+    | (name, Schema.VUnion literals) <- Map.toList constants,+      all Schema.isLiteral literals+    ]++buildObjectSignatureMap :: Map.Map String Schema.ConvexType -> ObjectSignatureMap+buildObjectSignatureMap constants =+  Map.fromList+    [ (sortOn fst fields, name)+    | (name, Schema.VObject fields) <- Map.toList constants+    ]++unifyFunctionTypes :: (Schema.ConvexType -> Schema.ConvexType) -> Action.ConvexFunction -> Action.ConvexFunction+unifyFunctionTypes unifyType func =+  func+    { Action.funcArgs = map (\(name, t) -> (name, unifyType t)) (Action.funcArgs func),+      Action.funcReturn = unifyType (Action.funcReturn func)+    }++unifyTypeRecursively :: Maybe String -> UnionSignatureMap -> ObjectSignatureMap -> Schema.ConvexType -> Schema.ConvexType+unifyTypeRecursively mCurrentName unionMap objectMap = go+  where+    goRec = unifyTypeRecursively Nothing unionMap objectMap++    go u@(Schema.VUnion literals)       | all Schema.isLiteral literals =           let signature = sort $ map Schema.getLiteralString literals            in case Map.lookup signature unionMap of-                Just refName -> Schema.VReference refName-                Nothing -> u-      | otherwise = u-    unifyTypeRecursively unionMap (Schema.VObject fields) =-      Schema.VObject $ map (\(name, t) -> (name, unifyTypeRecursively unionMap t)) fields-    unifyTypeRecursively unionMap (Schema.VArray inner) =-      Schema.VArray $ unifyTypeRecursively unionMap inner-    unifyTypeRecursively unionMap (Schema.VOptional inner) =-      Schema.VOptional $ unifyTypeRecursively unionMap inner-    unifyTypeRecursively _ otherType = otherType -- Base cases+                Just refName ->+                  if Just refName == mCurrentName+                    then canonicalizeType u+                    else Schema.VReference refName+                Nothing -> canonicalizeType u+      | otherwise =+          let unifiedUnion = Schema.VUnion (map goRec literals)+           in canonicalizeType unifiedUnion+    go (Schema.VObject fields) =+      let unifiedFields = map (\(name, t) -> (name, goRec t)) fields+          canonicalAttempt = canonicalizeType (Schema.VObject unifiedFields)+       in case canonicalAttempt of+            Schema.VObject signature ->+              let canonicalObject = Schema.VObject signature+               in case Map.lookup signature objectMap of+                    Just refName ->+                      if Just refName == mCurrentName+                        then canonicalObject+                        else Schema.VReference refName+                    Nothing -> canonicalObject+            _ -> canonicalAttempt -- Should not happen, but safer+    go (Schema.VArray inner) = Schema.VArray (goRec inner)+    go (Schema.VOptional inner) = Schema.VOptional (goRec inner)+    go otherType = otherType++unifyOnce :: ParsedProject -> ParsedProject+unifyOnce project =+  let unionMap = buildUnionSignatureMap (ppConstants project)+      initialObjectMap = buildObjectSignatureMap (ppConstants project)++      -- First, unify the constants themselves. This resolves nested anonymous objects+      -- within the constants first, creating a canonical representation for this pass.+      unifiedConstants =+        Map.mapWithKey (\k -> unifyTypeRecursively (Just k) unionMap initialObjectMap) (ppConstants project)++      -- Now, build the object map for function unification from these *newly unified* constants.+      -- This map contains the canonical object structures for this pass.+      finalObjectMap = buildObjectSignatureMap unifiedConstants++      -- Create the unification function for anonymous types found in function signatures.+      unifyAnonType = unifyTypeRecursively Nothing unionMap finalObjectMap++      unifiedFunctions = map (unifyFunctionTypes unifyAnonType) (ppFunctions project)+   in project+        { ppConstants = unifiedConstants,+          ppFunctions = unifiedFunctions+        }
test/ActionParserTest.hs view
@@ -3,10 +3,65 @@ module ActionParserTest (tests) where  import qualified Convex.Action.Parser as Action+import qualified Convex.Parser as Parser import qualified Convex.Schema.Parser as Schema+import qualified Data.Map as Map import Test.HUnit import Text.Parsec (runParserT) +-- A dedicated test runner for unification tests that first parses a schema.+runSchemaUnificationTest ::+  String ->+  String ->+  String ->+  [Action.ConvexFunction] ->+  Test+runSchemaUnificationTest testName schemaString actionString expected =+  testName ~: TestCase $ do+    -- First, parse the schema to get the constants.+    schemaResult <- Schema.parseSchema schemaString+    case schemaResult of+      Left err -> assertFailure ("Schema parser failed: " ++ show err)+      Right parsedSchemaFile -> do+        let initialState = Schema.ParserState {Schema.psConstants = Schema.parsedConstants parsedSchemaFile}+        -- Now, parse the action file with the constants from the schema.+        actionResult <- runParserT (Action.parseActionFile "testPath") initialState "(test)" actionString+        case actionResult of+          Left err -> assertFailure ("Action parser failed: " ++ show err)+          Right funcs ->+            let project =+                  Parser.ParsedProject+                    { Parser.ppSchema = Schema.parsedSchema parsedSchemaFile,+                      Parser.ppConstants = Schema.parsedConstants parsedSchemaFile,+                      Parser.ppFunctions = funcs+                    }+                unifiedProject = Parser.runUnificationPass project+             in Parser.ppFunctions unifiedProject @?= expected++-- A dedicated test runner for unification tests.+runUnificationTest ::+  String ->+  String ->+  Map.Map String Schema.ConvexType ->+  String ->+  [Action.ConvexFunction] ->+  Test+runUnificationTest testName path constants input expected =+  testName ~: TestCase $ do+    let initialState = Schema.ParserState {Schema.psConstants = constants}+    result <- runParserT (Action.parseActionFile path) initialState "(test)" input+    case result of+      Left err -> assertFailure ("Parser failed: " ++ show err)+      Right funcs ->+        let project =+              Parser.ParsedProject+                { Parser.ppSchema = Schema.Schema {Schema.getTables = []},+                  Parser.ppConstants = constants,+                  Parser.ppFunctions = funcs+                }+            unifiedProject = Parser.runUnificationPass project+         in Parser.ppFunctions unifiedProject @?= expected+ tests :: Test tests =   "Action Definition Parser"@@ -18,7 +73,18 @@         runActionTest "parses internal action with external type as VAny" "functions/stripe" sampleStripeSubscriptionAction expectedStripeSubscriptionAction,         runActionTest "parses public action with external type" "functions/stripe" sampleStripeCheckoutAction sampleStripeCheckoutActionExpected,         runActionTest "parses public action with external type as VAny" "functions/stripe" sampleStripeSubscriptionActionPublic expectedStripeSubscriptionActionPublic,-        runActionTest "parses createAsset mutation with complex args" "functions/assets" sampleCreateAssetAction expectedCreateAssetAction+        runActionTest "parses createAsset mutation with complex args" "functions/assets" sampleCreateAssetAction expectedCreateAssetAction,+        runUnificationTest+          "unifies function return with named doc"+          "functions/users"+          userProfileConstants+          sampleGetUserAction+          expectedGetUserAction,+        runSchemaUnificationTest+          "unifies function return with table doc"+          sampleSchemaWithUserTable+          sampleGetUserTableAction+          expectedGetUserTableAction       ]   where     runActionTest testName path input expected =@@ -204,5 +270,141 @@             )           ],         Action.funcReturn = Schema.VId "assets"+      }+  ]++-- Test case for structural unification of a function's return type.+userProfileConstants :: Map.Map String Schema.ConvexType+userProfileConstants =+  Map.fromList+    [ ("UserProfile", userProfileType),+      ("Address", addressType)+    ]+  where+    addressType =+      Schema.VObject+        [ ("street", Schema.VString),+          ("city", Schema.VString)+        ]+    userProfileType =+      Schema.VObject+        [ ("name", Schema.VString),+          ("address", Schema.VReference "Address")+        ]++sampleGetUserAction :: String+sampleGetUserAction =+  unlines+    [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{",+      "  name: string;",+      "  address: {",+      "    street: string;",+      "    city: string;",+      "  };",+      "}>>;"+    ]++expectedGetUserAction :: [Action.ConvexFunction]+expectedGetUserAction =+  [ Action.ConvexFunction+      { Action.funcName = "getUser",+        Action.funcPath = "functions/users",+        Action.funcType = Action.Query,+        Action.funcArgs = [],+        Action.funcReturn = Schema.VReference "UserProfile" -- Should be unified+      }+  ]++-- Test case for unification against a table doc.+sampleSchemaWithUserTable :: String+sampleSchemaWithUserTable =+  unlines+    [ "import { defineSchema, defineTable } from \"convex/server\";",+      "import { v } from \"convex/values\";",+      "",+      "export default defineSchema({",+      "  users: defineTable({",+      "    name: v.string(),",+      "    email: v.string()",+      "  })",+      "});"+    ]++sampleGetUserTableAction :: String+sampleGetUserTableAction =+  unlines+    [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{",+      "  _id: import(\"convex/values\").GenericId<\"users\">;",+      "  _creationTime: number;",+      "  name: string;",+      "  email: string;",+      "}>>;"+    ]++expectedGetUserTableAction :: [Action.ConvexFunction]+expectedGetUserTableAction =+  [ Action.ConvexFunction+      { Action.funcName = "getUser",+        Action.funcPath = "testPath",+        Action.funcType = Action.Query,+        Action.funcArgs = [],+        Action.funcReturn = Schema.VReference "UsersDoc" -- Should be unified+      }+  ]++sampleSchemaWithAssetsTable :: String+sampleSchemaWithAssetsTable =+  unlines+    [ "import { defineSchema, defineTable } from \"convex/server\";",+      "import { v } from \"convex/values\";",+      "",+      "export default defineSchema({",+      "  projects: defineTable({",+      "    name: v.string()",+      "  }),",+      "  assets: defineTable({",+      "    project_id: v.id(\"projects\"),",+      "    asset_name: v.string(),",+      "    asset_essence_mtime: v.number(),",+      "    link_metadata: v.object({",+      "      sample_rate: v.number(),",+      "      summary: v.bytes(),",+      "      length: v.number()",+      "    })",+      "  })",+      "});"+    ]++sampleGetAssetsAction :: String+sampleGetAssetsAction =+  unlines+    [ "export declare const getAssets: import(\"convex/server\").RegisteredQuery<\"public\", {",+      "  project_id: import(\"convex/values\").Id<\"projects\">;",+      "  secret: string;",+      "}, Promise<Array<{",+      "  _id: import(\"convex/values\").Id<\"assets\">;",+      "  _creationTime: number;",+      "  project_id: import(\"convex/values\").Id<\"projects\">;",+      "  asset_name: string;",+      "  asset_essence_mtime: number;",+      "  link_metadata: {",+      "    summary: Uint8Array;",+      "    sample_rate: number;",+      "    length: number;",+      "  };",+      "}>>>;"+    ]++expectedGetAssetsAction :: [Action.ConvexFunction]+expectedGetAssetsAction =+  [ Action.ConvexFunction+      { Action.funcName = "getAssets",+        Action.funcPath = "testPath",+        Action.funcType = Action.Query,+        Action.funcArgs =+          [ ("project_id", Schema.VId "projects"),+            ("secret", Schema.VString)+          ],+        Action.funcReturn = Schema.VArray (Schema.VReference "AssetsDoc")       }   ]
test/Main.hs view
@@ -4,6 +4,7 @@ import qualified ApiParserTest import qualified RustSerializationTest import qualified SchemaParserTest+import qualified UnificationTest import System.Exit (exitFailure, exitSuccess) import Test.HUnit @@ -15,7 +16,8 @@           [ ApiParserTest.tests,             ActionParserTest.tests,             SchemaParserTest.tests,-            RustSerializationTest.tests+            RustSerializationTest.tests,+            UnificationTest.tests           ]    -- Run the combined test suite
+ test/UnificationTest.hs view
@@ -0,0 +1,237 @@+{-# LANGUAGE OverloadedStrings #-}++module UnificationTest (tests) where++import qualified Convex.Action.Parser as Action+import qualified Convex.Parser as Parser+import qualified Convex.Schema.Parser as Schema+import Test.HUnit++testUnifyWithTableDoc :: Test+testUnifyWithTableDoc = "unifies function return with table doc" ~: TestCase $ do+  let schemaContent =+        unlines+          [ "import { defineSchema, defineTable } from \"convex/server\";",+            "import { v } from \"convex/values\";",+            "",+            "export default defineSchema({",+            "  users: defineTable({",+            "    name: v.string(),",+            "    email: v.string()",+            "  })",+            "});"+          ]+  let apiFileContent =+        unlines+          [ "declare const fullApi: ApiFromModules<{",+            "  \"myActions\": typeof myActions;",+            "}>;"+          ]+  let actionContent =+        unlines+          [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{ _id: import(\"convex/values\").GenericId<\"users\">; _creationTime: number; name: string; email: string; }>>;"+          ]+  let actionContents = [("myActions", actionContent)]++  result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+  case result of+    Left err -> assertFailure ("Parser failed: " ++ show err)+    Right project ->+      let expected =+            [ Action.ConvexFunction+                { Action.funcName = "getUser",+                  Action.funcPath = "myActions",+                  Action.funcType = Action.Query,+                  Action.funcArgs = [],+                  Action.funcReturn = Schema.VReference "UsersDoc"+                }+            ]+       in Parser.ppFunctions project @?= expected++testUnifyWithNestedObject :: Test+testUnifyWithNestedObject = "unifies function return with nested object" ~: TestCase $ do+  let schemaContent =+        unlines+          [ "import { defineSchema, defineTable } from \"convex/server\";",+            "import { v } from \"convex/values\";",+            "",+            "export default defineSchema({",+            "  users: defineTable({",+            "    name: v.string(),",+            "    profile: v.object({",+            "      image: v.string()",+            "    })",+            "  })",+            "});"+          ]+  let apiFileContent =+        unlines+          [ "declare const fullApi: ApiFromModules<{",+            "  \"myActions\": typeof myActions;",+            "}>;"+          ]+  let actionContent =+        unlines+          [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{ _id: import(\"convex/values\").GenericId<\"users\">; _creationTime: number; name: string; profile: { image: string; }; }>>;"+          ]+  let actionContents = [("myActions", actionContent)]++  result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+  case result of+    Left err -> assertFailure ("Parser failed: " ++ show err)+    Right project ->+      let expected =+            [ Action.ConvexFunction+                { Action.funcName = "getUser",+                  Action.funcPath = "myActions",+                  Action.funcType = Action.Query,+                  Action.funcArgs = [],+                  Action.funcReturn = Schema.VReference "UsersDoc"+                }+            ]+       in Parser.ppFunctions project @?= expected++testUnifyWithArrayOfDocs :: Test+testUnifyWithArrayOfDocs = "unifies function returning an array of docs" ~: TestCase $ do+  let schemaContent =+        unlines+          [ "import { defineSchema, defineTable } from \"convex/server\";",+            "import { v } from \"convex/values\";",+            "",+            "export default defineSchema({",+            "  users: defineTable({",+            "    name: v.string(),",+            "    email: v.string()",+            "  })",+            "});"+          ]+  let apiFileContent =+        unlines+          [ "declare const fullApi: ApiFromModules<{",+            "  \"myActions\": typeof myActions;",+            "}>;"+          ]+  let actionContent =+        unlines+          [ "export declare const listUsers: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{",+            "  _id: import(\"convex/values\").GenericId<\"users\">;",+            "  _creationTime: number;",+            "  name: string;",+            "  email: string;",+            "}[]>>;"+          ]+  let actionContents = [("myActions", actionContent)]++  result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+  case result of+    Left err -> assertFailure ("Parser failed: " ++ show err)+    Right project ->+      let expected =+            [ Action.ConvexFunction+                { Action.funcName = "listUsers",+                  Action.funcPath = "myActions",+                  Action.funcType = Action.Query,+                  Action.funcArgs = [],+                  Action.funcReturn = Schema.VArray (Schema.VReference "UsersDoc")+                }+            ]+       in Parser.ppFunctions project @?= expected++testUnifyWithDeeplyNestedObject :: Test+testUnifyWithDeeplyNestedObject = "unifies function return with deeply nested object" ~: TestCase $ do+  let schemaContent =+        unlines+          [ "import { defineSchema, defineTable } from \"convex/server\";",+            "import { v } from \"convex/values\";",+            "",+            "export default defineSchema({",+            "  users: defineTable({",+            "    name: v.string(),",+            "    profile: v.object({",+            "      image: v.string(),",+            "      details: v.object({",+            "        bio: v.string()",+            "      })",+            "    })",+            "  })",+            "});"+          ]+  let apiFileContent =+        unlines+          [ "declare const fullApi: ApiFromModules<{",+            "  \"myActions\": typeof myActions;",+            "}>;"+          ]+  let actionContent =+        unlines+          [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{ _id: import(\"convex/values\").GenericId<\"users\">; _creationTime: number; name: string; profile: { image: string; details: { bio: string; } }; }>>;"+          ]+  let actionContents = [("myActions", actionContent)]++  result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+  case result of+    Left err -> assertFailure ("Parser failed: " ++ show err)+    Right project ->+      let expected =+            [ Action.ConvexFunction+                { Action.funcName = "getUser",+                  Action.funcPath = "myActions",+                  Action.funcType = Action.Query,+                  Action.funcArgs = [],+                  Action.funcReturn = Schema.VReference "UsersDoc"+                }+            ]+       in Parser.ppFunctions project @?= expected++tests :: Test+tests =+  "Unification"+    ~: test+      [ testUnifyWithTableDoc,+        testUnifyWithNestedObject,+        testUnifyWithArrayOfDocs,+        testUnifyWithDeeplyNestedObject,+        testUnifyWithShuffledFields+      ]++testUnifyWithShuffledFields :: Test+testUnifyWithShuffledFields = "unifies function return with shuffled fields" ~: TestCase $ do+  let schemaContent =+        unlines+          [ "import { defineSchema, defineTable } from \"convex/server\";",+            "import { v } from \"convex/values\";",+            "",+            "export default defineSchema({",+            "  users: defineTable({",+            "    name: v.string(),",+            "    email: v.string()",+            "  })",+            "});"+          ]+  let apiFileContent =+        unlines+          [ "declare const fullApi: ApiFromModules<{",+            "  \"myActions\": typeof myActions;",+            "}>;"+          ]+  let actionContent =+        unlines+          -- Note the shuffled fields: email, name instead of name, email+          [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{ _id: import(\"convex/values\").GenericId<\"users\">; _creationTime: number; email: string; name: string; }>>;"+          ]+  let actionContents = [("myActions", actionContent)]++  result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+  case result of+    Left err -> assertFailure ("Parser failed: " ++ show err)+    Right project ->+      let expected =+            [ Action.ConvexFunction+                { Action.funcName = "getUser",+                  Action.funcPath = "myActions",+                  Action.funcType = Action.Query,+                  Action.funcArgs = [],+                  Action.funcReturn = Schema.VReference "UsersDoc"+                }+            ]+       in Parser.ppFunctions project @?= expected