mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-09-11 15:26:19 +03:00
Ensure Auth entity has a unique username (#1257)
This commit is contained in:
parent
c5540ae742
commit
90bbff3b64
@ -1,6 +1,6 @@
|
||||
app crudTesting {
|
||||
wasp: {
|
||||
version: "^0.10.4"
|
||||
version: "^0.11.0"
|
||||
},
|
||||
head: [
|
||||
"<link rel=\"stylesheet\" href=\"https://unpkg.com/mvp.css@1.12/mvp.css\">"
|
||||
|
@ -7,15 +7,19 @@ module Wasp.AppSpec.Entity
|
||||
getPslModelBody,
|
||||
getIdField,
|
||||
getIdBlockAttribute,
|
||||
isFieldUnique,
|
||||
-- only for testing:
|
||||
doesFieldHaveAttribute,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Data (Data)
|
||||
import Data.List (find)
|
||||
import Wasp.AppSpec.Core.Decl (IsDecl)
|
||||
import Wasp.AppSpec.Entity.Field (Field)
|
||||
import qualified Wasp.AppSpec.Entity.Field as Field
|
||||
import qualified Wasp.Psl.Ast.Model as PslModel
|
||||
import Wasp.Psl.Util (findIdBlockAttribute, findIdField)
|
||||
import Wasp.Psl.Util (doesPslFieldHaveAttribute, findIdBlockAttribute, findIdField)
|
||||
|
||||
data Entity = Entity
|
||||
{ fields :: ![Field],
|
||||
@ -45,5 +49,18 @@ getPslModelBody = pslModelBody
|
||||
getIdField :: Entity -> Maybe PslModel.Field
|
||||
getIdField = findIdField . getPslModelBody
|
||||
|
||||
isFieldUnique :: String -> Entity -> Maybe Bool
|
||||
isFieldUnique fieldName = doesFieldHaveAttribute fieldName "unique"
|
||||
|
||||
doesFieldHaveAttribute :: String -> String -> Entity -> Maybe Bool
|
||||
doesFieldHaveAttribute fieldName attrName entity =
|
||||
doesPslFieldHaveAttribute attrName <$> findPslFieldByName fieldName entity
|
||||
|
||||
findPslFieldByName :: String -> Entity -> Maybe PslModel.Field
|
||||
findPslFieldByName fieldName Entity {pslModelBody = PslModel.Body elements} =
|
||||
find isField [field | (PslModel.ElementField field) <- elements]
|
||||
where
|
||||
isField PslModel.Field {_name = name} = name == fieldName
|
||||
|
||||
getIdBlockAttribute :: Entity -> Maybe PslModel.Attribute
|
||||
getIdBlockAttribute = findIdBlockAttribute . getPslModelBody
|
||||
|
@ -27,6 +27,7 @@ import qualified Wasp.AppSpec.App.Db as AS.Db
|
||||
import qualified Wasp.AppSpec.App.Wasp as Wasp
|
||||
import Wasp.AppSpec.Core.Decl (takeDecls)
|
||||
import qualified Wasp.AppSpec.Crud as AS.Crud
|
||||
import Wasp.AppSpec.Entity (isFieldUnique)
|
||||
import qualified Wasp.AppSpec.Entity as Entity
|
||||
import qualified Wasp.AppSpec.Entity.Field as Entity.Field
|
||||
import qualified Wasp.AppSpec.Page as Page
|
||||
@ -149,14 +150,37 @@ validateAuthUserEntityHasCorrectFieldsIfUsernameAndPasswordAuthIsUsed spec = cas
|
||||
Just auth ->
|
||||
if not $ Auth.isUsernameAndPasswordAuthEnabled auth
|
||||
then []
|
||||
else
|
||||
let userEntity = snd $ AS.resolveRef spec (Auth.userEntity auth)
|
||||
userEntityFields = Entity.getFields userEntity
|
||||
in concatMap
|
||||
(validateEntityHasField "app.auth.userEntity" userEntityFields)
|
||||
[ ("username", Entity.Field.FieldTypeScalar Entity.Field.String, "String"),
|
||||
("password", Entity.Field.FieldTypeScalar Entity.Field.String, "String")
|
||||
]
|
||||
else validationErrors
|
||||
where
|
||||
validationErrors = concat [usernameValidationErrors, passwordValidationErrors]
|
||||
usernameValidationErrors
|
||||
| not $ null usernameTypeValidationErrors = usernameTypeValidationErrors
|
||||
| otherwise = usernameAttributeValidationErrors
|
||||
passwordValidationErrors =
|
||||
validateEntityHasField
|
||||
userEntityName
|
||||
authUserEntityPath
|
||||
userEntityFields
|
||||
("password", Entity.Field.FieldTypeScalar Entity.Field.String, "String")
|
||||
usernameTypeValidationErrors =
|
||||
validateEntityHasField
|
||||
userEntityName
|
||||
authUserEntityPath
|
||||
userEntityFields
|
||||
("username", Entity.Field.FieldTypeScalar Entity.Field.String, "String")
|
||||
usernameAttributeValidationErrors
|
||||
| isFieldUnique "username" userEntity == Just True = []
|
||||
| otherwise =
|
||||
[ GenericValidationError $
|
||||
"The field 'username' on entity '"
|
||||
++ userEntityName
|
||||
++ "' (referenced by "
|
||||
++ authUserEntityPath
|
||||
++ ") must be marked with the '@unique' attribute."
|
||||
]
|
||||
userEntityFields = Entity.getFields userEntity
|
||||
authUserEntityPath = "app.auth.userEntity"
|
||||
(userEntityName, userEntity) = AS.resolveRef spec (Auth.userEntity auth)
|
||||
|
||||
validateAuthUserEntityHasCorrectFieldsIfEmailAuthIsUsed :: AppSpec -> [ValidationError]
|
||||
validateAuthUserEntityHasCorrectFieldsIfEmailAuthIsUsed spec = case App.auth (snd $ getApp spec) of
|
||||
@ -165,10 +189,10 @@ validateAuthUserEntityHasCorrectFieldsIfEmailAuthIsUsed spec = case App.auth (sn
|
||||
if not $ Auth.isEmailAuthEnabled auth
|
||||
then []
|
||||
else
|
||||
let userEntity = snd $ AS.resolveRef spec (Auth.userEntity auth)
|
||||
let (userEntityName, userEntity) = AS.resolveRef spec (Auth.userEntity auth)
|
||||
userEntityFields = Entity.getFields userEntity
|
||||
in concatMap
|
||||
(validateEntityHasField "app.auth.userEntity" userEntityFields)
|
||||
(validateEntityHasField userEntityName "app.auth.userEntity" userEntityFields)
|
||||
[ ("email", Entity.Field.FieldTypeComposite (Entity.Field.Optional Entity.Field.String), "String"),
|
||||
("password", Entity.Field.FieldTypeComposite (Entity.Field.Optional Entity.Field.String), "String"),
|
||||
("isEmailVerified", Entity.Field.FieldTypeScalar Entity.Field.Boolean, "Boolean"),
|
||||
@ -203,7 +227,7 @@ validateExternalAuthEntityHasCorrectFieldsIfExternalAuthIsUsed spec = case App.a
|
||||
externalAuthEntityFields = Entity.getFields externalAuthEntity
|
||||
externalAuthEntityValidationErrors =
|
||||
concatMap
|
||||
(validateEntityHasField "app.auth.externalAuthEntity" externalAuthEntityFields)
|
||||
(validateEntityHasField externalAuthEntityName "app.auth.externalAuthEntity" externalAuthEntityFields)
|
||||
[ ("provider", Entity.Field.FieldTypeScalar Entity.Field.String, "String"),
|
||||
("providerId", Entity.Field.FieldTypeScalar Entity.Field.String, "String"),
|
||||
("user", Entity.Field.FieldTypeScalar (Entity.Field.UserType userEntityName), userEntityName),
|
||||
@ -211,20 +235,23 @@ validateExternalAuthEntityHasCorrectFieldsIfExternalAuthIsUsed spec = case App.a
|
||||
]
|
||||
userEntityValidationErrors =
|
||||
concatMap
|
||||
(validateEntityHasField "app.auth.userEntity" userEntityFields)
|
||||
[ ("externalAuthAssociations", Entity.Field.FieldTypeComposite $ Entity.Field.List $ Entity.Field.UserType externalAuthEntityName, externalAuthEntityName ++ "[]")
|
||||
(validateEntityHasField userEntityName "app.auth.userEntity" userEntityFields)
|
||||
[ ( "externalAuthAssociations",
|
||||
Entity.Field.FieldTypeComposite $ Entity.Field.List $ Entity.Field.UserType externalAuthEntityName,
|
||||
externalAuthEntityName ++ "[]"
|
||||
)
|
||||
]
|
||||
in externalAuthEntityValidationErrors ++ userEntityValidationErrors
|
||||
|
||||
validateEntityHasField :: String -> [Entity.Field.Field] -> (String, Entity.Field.FieldType, String) -> [ValidationError]
|
||||
validateEntityHasField entityName entityFields (fieldName, fieldType, fieldTypeName) =
|
||||
let maybeField = find ((== fieldName) . Entity.Field.fieldName) entityFields
|
||||
validateEntityHasField :: String -> String -> [Entity.Field.Field] -> (String, Entity.Field.FieldType, String) -> [ValidationError]
|
||||
validateEntityHasField entityName authEntityPath entityFields (fieldName, fieldType, fieldTypeName) =
|
||||
let maybeField = findFieldByName fieldName entityFields
|
||||
in case maybeField of
|
||||
Just providerField
|
||||
| Entity.Field.fieldType providerField == fieldType -> []
|
||||
_ ->
|
||||
[ GenericValidationError $
|
||||
"Expected an Entity referenced by " ++ entityName ++ " to have field '" ++ fieldName ++ "' of type '" ++ fieldTypeName ++ "'."
|
||||
"Entity '" ++ entityName ++ "' (referenced by " ++ authEntityPath ++ ") must have field '" ++ fieldName ++ "' of type '" ++ fieldTypeName ++ "'."
|
||||
]
|
||||
|
||||
validateApiRoutesAreUnique :: AppSpec -> [ValidationError]
|
||||
@ -276,12 +303,26 @@ validateCrudOperations spec =
|
||||
checkIfSimpleIdFieldIsDefinedForEntity :: (String, AS.Crud.Crud) -> [ValidationError]
|
||||
checkIfSimpleIdFieldIsDefinedForEntity (crudName, crud) = case (maybeIdField, maybeIdBlockAttribute) of
|
||||
(Just _, Nothing) -> []
|
||||
(Nothing, Just _) -> [GenericValidationError $ "Entity referenced by \"" ++ crudName ++ "\" CRUD declaration must have an ID field (marked with @id attribute) and not a composite ID (defined with @@id attribute)."]
|
||||
_missingIdFieldWithoutBlockIdAttributeDefined -> [GenericValidationError $ "Entity referenced by \"" ++ crudName ++ "\" CRUD declaration must have an ID field (marked with @id attribute)."]
|
||||
(Nothing, Just _) ->
|
||||
[ GenericValidationError $
|
||||
"Entity '"
|
||||
++ entityName
|
||||
++ "' (referenced by CRUD declaration '"
|
||||
++ crudName
|
||||
++ "') must have an ID field (specified with the '@id' attribute) and not a composite ID (specified with the '@@id' attribute)."
|
||||
]
|
||||
_missingIdFieldWithoutBlockIdAttributeDefined ->
|
||||
[ GenericValidationError $
|
||||
"Entity '"
|
||||
++ entityName
|
||||
++ "' (referenced by CRUD declaration '"
|
||||
++ crudName
|
||||
++ "') must have an ID field (specified with the '@id' attribute)."
|
||||
]
|
||||
where
|
||||
maybeIdField = Entity.getIdField entity
|
||||
maybeIdBlockAttribute = Entity.getIdBlockAttribute entity
|
||||
(_, entity) = AS.resolveRef spec (AS.Crud.entity crud)
|
||||
(entityName, entity) = AS.resolveRef spec (AS.Crud.entity crud)
|
||||
|
||||
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
|
||||
-- TODO: It would be great if we could ensure this at type level, but we decided that was too much work for now.
|
||||
@ -313,7 +354,10 @@ doesUserEntityContainField spec fieldName = do
|
||||
auth <- App.auth (snd $ getApp spec)
|
||||
let userEntity = snd $ AS.resolveRef spec (Auth.userEntity auth)
|
||||
let userEntityFields = Entity.getFields userEntity
|
||||
Just $ any (\field -> Entity.Field.fieldName field == fieldName) userEntityFields
|
||||
Just $ isJust $ findFieldByName fieldName userEntityFields
|
||||
|
||||
findFieldByName :: String -> [Entity.Field.Field] -> Maybe Entity.Field.Field
|
||||
findFieldByName name = find ((== name) . Entity.Field.fieldName)
|
||||
|
||||
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
|
||||
-- We validated that entity field exists, so we can safely use fromJust here.
|
||||
|
@ -26,3 +26,6 @@ findIdBlockAttribute (PslModel.Body elements) = find isIdBlockAttribute attribut
|
||||
-- We define the ID block attribute as an attribute with the name @@id.
|
||||
idBlockAttributeName :: String
|
||||
idBlockAttributeName = "id"
|
||||
|
||||
doesPslFieldHaveAttribute :: String -> PslModel.Field -> Bool
|
||||
doesPslFieldHaveAttribute name PslModel.Field {_attrs = attrs} = any ((== name) . PslModel._attrName) attrs
|
||||
|
@ -8,10 +8,26 @@ import qualified Wasp.Psl.Ast.Model as PslModel
|
||||
spec_AppSpecEntityTest :: Spec
|
||||
spec_AppSpecEntityTest = do
|
||||
describe "getIdField" $ do
|
||||
it "gets primary field from entity when it exists" $ do
|
||||
it "Gets primary field from entity when it exists" $ do
|
||||
getIdField entityWithIdField `shouldBe` Just idField
|
||||
it "returns Nothing if primary field doesn't exist" $ do
|
||||
it "Returns Nothing if primary field doesn't exist" $ do
|
||||
getIdField entityWithoutIdField `shouldBe` Nothing
|
||||
|
||||
describe "isFieldUnique" $ do
|
||||
it "Returns Nothing if the field doesn't exist on the entity" $ do
|
||||
Entity.isFieldUnique "nonExistingField" entityWithoutIdField `shouldBe` Nothing
|
||||
it "Returns Just False if the field exists on the entity but isn't unique" $ do
|
||||
Entity.isFieldUnique "description" entityWithIdField `shouldBe` Just False
|
||||
it "Returns Just True if the field exists and is unique" $ do
|
||||
Entity.isFieldUnique "id" entityWithIdField `shouldBe` Just True
|
||||
|
||||
describe "doesFieldHaveAttribute" $ do
|
||||
it "Returns Nothing if the field doesn't exist on the entity" $ do
|
||||
Entity.doesFieldHaveAttribute "nonExistingField" "unique" entityWithoutIdField `shouldBe` Nothing
|
||||
it "Returns Just False if the field exists on the entity but doesn't have the required attribute" $ do
|
||||
Entity.doesFieldHaveAttribute "description" "id" entityWithIdField `shouldBe` Just False
|
||||
it "Returns Just True if the field exists on the entity and has the required attribute" $ do
|
||||
Entity.doesFieldHaveAttribute "id" "id" entityWithIdField `shouldBe` Just True
|
||||
where
|
||||
entityWithIdField =
|
||||
Entity.makeEntity $
|
||||
@ -33,6 +49,10 @@ spec_AppSpecEntityTest = do
|
||||
[ PslModel.Attribute
|
||||
{ PslModel._attrName = "id",
|
||||
PslModel._attrArgs = []
|
||||
},
|
||||
PslModel.Attribute
|
||||
{ PslModel._attrName = "unique",
|
||||
PslModel._attrArgs = []
|
||||
}
|
||||
],
|
||||
PslModel._typeModifiers = []
|
||||
|
@ -21,6 +21,7 @@ import qualified Wasp.AppSpec.Page as AS.Page
|
||||
import qualified Wasp.AppSpec.Route as AS.Route
|
||||
import qualified Wasp.AppSpec.Valid as ASV
|
||||
import qualified Wasp.Psl.Ast.Model as PslM
|
||||
import qualified Wasp.Psl.Ast.Model as PslModel
|
||||
import qualified Wasp.SemanticVersion as SV
|
||||
import qualified Wasp.Version as WV
|
||||
|
||||
@ -87,10 +88,22 @@ spec_AppSpecValid = do
|
||||
|
||||
describe "auth-related validation" $ do
|
||||
let userEntityName = "User"
|
||||
let validUserField =
|
||||
PslModel.Field
|
||||
{ PslModel._name = "username",
|
||||
PslModel._type = PslModel.String,
|
||||
PslModel._attrs =
|
||||
[ PslModel.Attribute
|
||||
{ PslModel._attrName = "unique",
|
||||
PslModel._attrArgs = []
|
||||
}
|
||||
],
|
||||
PslModel._typeModifiers = []
|
||||
}
|
||||
let validUserEntity =
|
||||
AS.Entity.makeEntity
|
||||
( PslM.Body
|
||||
[ PslM.ElementField $ makeBasicPslField "username" PslM.String,
|
||||
[ PslM.ElementField validUserField,
|
||||
PslM.ElementField $ makeBasicPslField "password" PslM.String
|
||||
]
|
||||
)
|
||||
@ -224,7 +237,13 @@ spec_AppSpecValid = do
|
||||
let invalidUserEntity2 =
|
||||
AS.Entity.makeEntity
|
||||
( PslM.Body
|
||||
[ PslM.ElementField $ makeBasicPslField "username" PslM.String
|
||||
[PslM.ElementField validUserField]
|
||||
)
|
||||
let invalidUserEntity3 =
|
||||
AS.Entity.makeEntity
|
||||
( PslM.Body
|
||||
[ PslM.ElementField $ makeBasicPslField "username" PslM.String,
|
||||
PslM.ElementField $ makeBasicPslField "password" PslM.String
|
||||
]
|
||||
)
|
||||
|
||||
@ -236,11 +255,15 @@ spec_AppSpecValid = do
|
||||
it "returns an error if app.auth is set and user entity is of invalid shape" $ do
|
||||
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity)
|
||||
`shouldBe` [ ASV.GenericValidationError
|
||||
"Expected an Entity referenced by app.auth.userEntity to have field 'username' of type 'String'."
|
||||
"Entity 'User' (referenced by app.auth.userEntity) must have field 'username' of type 'String'."
|
||||
]
|
||||
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity2)
|
||||
`shouldBe` [ ASV.GenericValidationError
|
||||
"Expected an Entity referenced by app.auth.userEntity to have field 'password' of type 'String'."
|
||||
"Entity 'User' (referenced by app.auth.userEntity) must have field 'password' of type 'String'."
|
||||
]
|
||||
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity3)
|
||||
`shouldBe` [ ASV.GenericValidationError
|
||||
"The field 'username' on entity 'User' (referenced by app.auth.userEntity) must be marked with the '@unique' attribute."
|
||||
]
|
||||
where
|
||||
makeBasicPslField name typ = makePslField name typ False
|
||||
|
Loading…
Reference in New Issue
Block a user