From 8f136fb5cdd2d02748bf3dd79555a76756cf0487 Mon Sep 17 00:00:00 2001 From: Mihovil Ilakovac Date: Tue, 9 Jul 2024 23:44:56 +0200 Subject: [PATCH] Make sure `entity` declaration is not used in the Wasp file (#2152) --- waspc/src/Wasp/Analyzer.hs | 20 ++++++ waspc/src/Wasp/Analyzer/AnalyzeError.hs | 2 + waspc/src/Wasp/Analyzer/Parser/Valid.hs | 26 +++++++ .../Analyzer/StdTypeDefinitions/Entity.hs | 10 ++- .../Wasp/Analyzer/TypeChecker/TypeError.hs | 12 ++-- waspc/test/Analyzer/ValidTest.hs | 71 +++++++++++++++++++ waspc/waspc.cabal | 2 + waspc/waspls/src/Wasp/LSP/Diagnostic.hs | 1 + 8 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 waspc/src/Wasp/Analyzer/Parser/Valid.hs create mode 100644 waspc/test/Analyzer/ValidTest.hs diff --git a/waspc/src/Wasp/Analyzer.hs b/waspc/src/Wasp/Analyzer.hs index 98b144aaf..3f86d3407 100644 --- a/waspc/src/Wasp/Analyzer.hs +++ b/waspc/src/Wasp/Analyzer.hs @@ -127,6 +127,7 @@ import Wasp.Analyzer.AnalyzeError ) import Wasp.Analyzer.Evaluator (Decl, evaluate, takeDecls) import Wasp.Analyzer.Parser (parseStatements) +import Wasp.Analyzer.Parser.Valid (validateAst) import Wasp.Analyzer.Prisma (injectEntitiesFromPrismaSchema) import Wasp.Analyzer.StdTypeDefinitions (stdTypes) import Wasp.Analyzer.TypeChecker (typeCheck) @@ -138,6 +139,25 @@ import qualified Wasp.Psl.Ast.Schema as Psl.Schema analyze :: Psl.Schema.Schema -> String -> Either [AnalyzeError] [Decl] analyze prismaSchemaAst = (left (map ParseError) . parseStatements) + {-- + Why introduce AST validation and not just throw a ParseError from the parser? + + We want to support the `entity` declaration in the AST but not in the Wasp source + file. + + This was the fastest and cleanest (e.g. not having to hack the type checker) way + to allow users to define entities in the Prisma schema file. We are parsing + the `schema.prisma` file and injecting the models into the Wasp AST as entity + statements. + + We validate the AST to prevent users from defining entities in the Wasp source + file since we don't want to allow defining entities in two places. + + Wasp file -(parse)-> AST -(validate)-> AST -(injectEntities)-> AST (...) + ^ disallow entities here + ^ inject entities here + --} + >=> (left ((: []) . ValidationError) . validateAst) >=> injectEntitiesFromPrismaSchema prismaSchemaAst >=> (left ((: []) . TypeError) . typeCheck stdTypes) >=> (left ((: []) . EvaluationError) . evaluate stdTypes) diff --git a/waspc/src/Wasp/Analyzer/AnalyzeError.hs b/waspc/src/Wasp/Analyzer/AnalyzeError.hs index 87a1a96be..1751d3bdd 100644 --- a/waspc/src/Wasp/Analyzer/AnalyzeError.hs +++ b/waspc/src/Wasp/Analyzer/AnalyzeError.hs @@ -16,10 +16,12 @@ data AnalyzeError = ParseError PE.ParseError | TypeError TE.TypeError | EvaluationError EE.EvaluationError + | ValidationError (String, Ctx) deriving (Show, Eq) getErrorMessageAndCtx :: AnalyzeError -> (String, Ctx) getErrorMessageAndCtx = \case ParseError e -> first (("Parse error:\n" ++) . indent 2) $ PE.getErrorMessageAndCtx e + ValidationError (msg, ctx) -> ("Validation error:\n" ++ indent 2 msg, ctx) TypeError e -> first (("Type error:\n" ++) . indent 2) $ TE.getErrorMessageAndCtx e EvaluationError e -> first (("Evaluation error:\n" ++) . indent 2) $ EE.getErrorMessageAndCtx e diff --git a/waspc/src/Wasp/Analyzer/Parser/Valid.hs b/waspc/src/Wasp/Analyzer/Parser/Valid.hs new file mode 100644 index 000000000..b1fb62b99 --- /dev/null +++ b/waspc/src/Wasp/Analyzer/Parser/Valid.hs @@ -0,0 +1,26 @@ +module Wasp.Analyzer.Parser.Valid + ( validateAst, + ) +where + +import Data.List (find) +import qualified Wasp.Analyzer.Parser as P +import Wasp.Analyzer.StdTypeDefinitions.Entity (entityDeclTypeName) + +validateAst :: P.AST -> Either (String, P.Ctx) P.AST +validateAst = validateNoEntityDeclInWaspFile + +validateNoEntityDeclInWaspFile :: P.AST -> Either (String, P.Ctx) P.AST +validateNoEntityDeclInWaspFile ast@(P.AST stmts) = case findEntityStmt stmts of + Just (P.WithCtx ctx _) -> Left (entitiesNoLongerSupportedError, ctx) + Nothing -> Right ast + where + findEntityStmt :: [P.WithCtx P.Stmt] -> Maybe (P.WithCtx P.Stmt) + findEntityStmt = + find + ( \(P.WithCtx _ (P.Decl declTypeName _ _)) -> declTypeName == entityDeclTypeName + ) + + entitiesNoLongerSupportedError :: String + entitiesNoLongerSupportedError = + "Entities can no longer be defined in the .wasp file. You should migrate your entities to the schema.prisma file. Read more: https://wasp-lang.dev/docs/migrate-from-0-13-to-0-14#migrate-to-the-new-schemaprisma-file" diff --git a/waspc/src/Wasp/Analyzer/StdTypeDefinitions/Entity.hs b/waspc/src/Wasp/Analyzer/StdTypeDefinitions/Entity.hs index cb6bab51c..5077205f0 100644 --- a/waspc/src/Wasp/Analyzer/StdTypeDefinitions/Entity.hs +++ b/waspc/src/Wasp/Analyzer/StdTypeDefinitions/Entity.hs @@ -1,7 +1,10 @@ {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -module Wasp.Analyzer.StdTypeDefinitions.Entity () where +module Wasp.Analyzer.StdTypeDefinitions.Entity + ( entityDeclTypeName, + ) +where import Control.Arrow (left) import Wasp.Analyzer.Evaluator.EvaluationError (mkEvaluationError) @@ -16,7 +19,7 @@ import qualified Wasp.Psl.Parser.Model instance IsDeclType Entity where declType = DeclType - { dtName = "entity", + { dtName = entityDeclTypeName, dtBodyType = Type.QuoterType "psl", dtEvaluate = \typeDefinitions bindings declName expr -> Decl.makeDecl @Entity declName <$> declEvaluate typeDefinitions bindings expr @@ -27,3 +30,6 @@ instance IsDeclType Entity where left (ER.mkEvaluationError ctx . ER.ParseError . ER.EvaluationParseErrorParsec) $ makeEntity <$> Wasp.Psl.Parser.Model.parseBody pslString _ -> Left $ mkEvaluationError ctx $ ER.ExpectedType (Type.QuoterType "psl") (TC.AST.exprType expr) + +entityDeclTypeName :: String +entityDeclTypeName = "entity" diff --git a/waspc/src/Wasp/Analyzer/TypeChecker/TypeError.hs b/waspc/src/Wasp/Analyzer/TypeChecker/TypeError.hs index b4c4912fb..cb39c24d9 100644 --- a/waspc/src/Wasp/Analyzer/TypeChecker/TypeError.hs +++ b/waspc/src/Wasp/Analyzer/TypeChecker/TypeError.hs @@ -27,15 +27,15 @@ data TypeError' -- We use "unify" in the TypeChecker when trying to infer the common type for -- typed expressions that we know should be of the same type (e.g. for -- elements in the list). - = UnificationError TypeCoercionError + = UnificationError TypeCoercionError -- | Type coercion error that occurs when trying to use the typed expression -- of type T1 where T2 is expected. If T2 is a super type of T1 and T1 can be -- safely coerced to T2, no problem, but if not, we get this error. - | CoercionError TypeCoercionError - | NoDeclarationType TypeName - | UndefinedIdentifier Identifier - | QuoterUnknownTag QuoterTag - | DictDuplicateField DictFieldName + | CoercionError TypeCoercionError + | NoDeclarationType TypeName + | UndefinedIdentifier Identifier + | QuoterUnknownTag QuoterTag + | DictDuplicateField DictFieldName deriving (Eq, Show) {- ORMOLU_ENABLE -} diff --git a/waspc/test/Analyzer/ValidTest.hs b/waspc/test/Analyzer/ValidTest.hs new file mode 100644 index 000000000..530d0d553 --- /dev/null +++ b/waspc/test/Analyzer/ValidTest.hs @@ -0,0 +1,71 @@ +module Analyzer.ValidTest where + +import Data.Either (fromRight, isRight) +import Test.Tasty.Hspec +import Wasp.Analyzer.Parser hiding (withCtx) +import qualified Wasp.Analyzer.Parser as P +import Wasp.Analyzer.Parser.Valid (validateAst) +import qualified Wasp.Version as WV + +spec_ValidateAst :: Spec +spec_ValidateAst = do + it "Returns an error when entities are used" $ do + validateAndParseSource (waspSourceLines ++ entityDeclarationLines) + `shouldBe` Left + ( "Entities can no longer be defined in the .wasp file. You should migrate your entities to the schema.prisma file. Read more: https://wasp-lang.dev/docs/migrate-from-0-13-to-0-14#migrate-to-the-new-schemaprisma-file", + P.Ctx + ( P.SourceRegion + (P.SourcePosition 34 1) + (P.SourcePosition 37 5) + ) + ) + + it "Returns AST when everything is correct" $ do + isRight (validateAndParseSource waspSourceLines) `shouldBe` True + where + validateAndParseSource = validateAst . parseSource + + parseSource = fromRight (error "Parsing went wrong") . parseStatements . unlines + + waspSourceLines = + [ "app Todo {", + " wasp: {", + " version: \"^" ++ show WV.waspVersion ++ "\",", + " },", + " title: \"Todo App\",", + " head: [\"foo\", \"bar\"],", + " auth: {", + " userEntity: User,", + " methods: {", + " usernameAndPassword: {", + " userSignupFields: import { getUserFields } from \"@src/auth/signup.js\",", + " }", + " },", + " onAuthFailedRedirectTo: \"/\",", + " },", + "}", + "", + "page HomePage {", + " component: import Home from \"@src/pages/Main\"", + "}", + "", + "route HomeRoute { path: \"/\", to: HomePage }", + "", + "query getUsers {", + " fn: import { getAllUsers } from \"@src/foo.js\",", + " entities: [User]", + "}", + "", + "action updateUser {", + " fn: import { updateUser } from \"@src/foo.js\",", + " entities: [User],", + " auth: true", + "}" + ] + + entityDeclarationLines = + [ "entity User {=psl", + " id Int @id @default(autoincrement())", + " email String @unique", + "psl=}" + ] diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 4839c394d..fe0ea50f5 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -201,6 +201,7 @@ library Wasp.Analyzer.Parser.SourceSpan Wasp.Analyzer.Parser.Token Wasp.Analyzer.Parser.TokenSet + Wasp.Analyzer.Parser.Valid Wasp.Analyzer.StdTypeDefinitions Wasp.Analyzer.StdTypeDefinitions.App.Dependency Wasp.Analyzer.StdTypeDefinitions.Entity @@ -612,6 +613,7 @@ test-suite waspc-test Analyzer.TestUtil Analyzer.TypeChecker.InternalTest Analyzer.TypeCheckerTest + Analyzer.ValidTest AnalyzerTest AppSpec.ValidTest AppSpec.EntityTest diff --git a/waspc/waspls/src/Wasp/LSP/Diagnostic.hs b/waspc/waspls/src/Wasp/LSP/Diagnostic.hs index 09e853d34..5c5d77456 100644 --- a/waspc/waspls/src/Wasp/LSP/Diagnostic.hs +++ b/waspc/waspls/src/Wasp/LSP/Diagnostic.hs @@ -108,6 +108,7 @@ waspErrorAsPrettyEditorMessage = Text.pack . fst . W.getErrorMessageAndCtx waspErrorSource :: W.AnalyzeError -> Text waspErrorSource (W.ParseError _) = "parse" +waspErrorSource (W.ValidationError _) = "validate" waspErrorSource (W.TypeError _) = "typecheck" waspErrorSource (W.EvaluationError _) = "evaluate"