Implemented wasp start db (managed dev db) (#1044)

Implemented 'wasp start db' + docs + e2e tests + refactoring.
This commit is contained in:
Martin Šošić 2023-03-21 16:37:20 +01:00 committed by GitHub
parent a604518da6
commit 91a8063081
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 1417 additions and 595 deletions

View File

@ -8,15 +8,9 @@ Here, we implement it in Wasp, by following their [specification](https://realwo
This app is deployed at https://wasp-rwa.netlify.app/ .
# Development
### Database
Wasp needs postgre database running - provide it with database connection URL via env var `DATABASE_URL` - best to do it via .env file.
Easy way to get going with postgresql database: run db with `docker run --rm --publish 5432:5432 -v postgresql-data:/var/lib/postgresql/data --env POSTGRES_PASSWORD=devpass postgres`.
`DATABASE_URL` in this case is `postgresql://postgres:devpass@localhost:5432/postgres`.
### Running
`wasp start`
`wasp start` to run the client and the server.
`wasp start db` to run the dev database.
## TODO

View File

@ -1,6 +1,6 @@
app Thoughts {
wasp: {
version: "^0.8.0"
version: "^0.9.1"
},
title: "Thoughts",
db: { system: PostgreSQL },

View File

@ -21,6 +21,15 @@ export const fooBar : FooBar = (req, res, context) => {
}
```
### `wasp start db` -> Wasp can now run your dev database for you with a single command
Moving from SQLite to PostgreSQL with Wasp can feel like increase in complexity, because suddenly you have to care about running your PostgreSQL database, providing connection URL for it via env var, and if you checkout somebody's else Wasp project, or your old Wasp project that you have no memory of any more, you also have to figure all that out.
To help with that, we now added `wasp start db`, which runs a development database for you!
That it, all you need to do is run `wasp start db` and you are good to go. No env var setting, no remembering how to run the db.
NOTE: Requires `docker` to be installed.
## v0.9.0
### BREAKING CHANGES

View File

@ -20,4 +20,4 @@ jobs: $ncpus
test-show-details: direct
-- WARNING: Run cabal update if your local package index is older than this date.
index-state: 2022-03-22T14:16:26Z
index-state: 2023-03-09T09:15:49Z

View File

@ -5,6 +5,7 @@ import qualified Control.Concurrent.Async as Async
import qualified Control.Exception as E
import Control.Monad (void)
import Data.Char (isSpace)
import Data.List (intercalate)
import Main.Utf8 (withUtf8)
import System.Environment (getArgs)
import Wasp.Cli.Command (runCommand)
@ -14,13 +15,16 @@ import qualified Wasp.Cli.Command.Call as Command.Call
import Wasp.Cli.Command.Clean (clean)
import Wasp.Cli.Command.Compile (compile)
import Wasp.Cli.Command.CreateNewProject (createNewProject)
import Wasp.Cli.Command.Db (runDbCommand, studio)
import Wasp.Cli.Command.Db (runDbCommand)
import qualified Wasp.Cli.Command.Db.Migrate as Command.Db.Migrate
import qualified Wasp.Cli.Command.Db.Reset as Command.Db.Reset
import qualified Wasp.Cli.Command.Db.Studio as Command.Db.Studio
import Wasp.Cli.Command.Deploy (deploy)
import Wasp.Cli.Command.Deps (deps)
import Wasp.Cli.Command.Dockerfile (printDockerfile)
import Wasp.Cli.Command.Info (info)
import Wasp.Cli.Command.Start (start)
import qualified Wasp.Cli.Command.Start.Db as Command.Start.Db
import qualified Wasp.Cli.Command.Telemetry as Telemetry
import Wasp.Cli.Command.Uninstall (uninstall)
import Wasp.Cli.Command.WaspLS (runWaspLS)
@ -35,6 +39,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
let commandCall = case args of
["new", projectName] -> Command.Call.New projectName
["start"] -> Command.Call.Start
["start", "db"] -> Command.Call.StartDb
["clean"] -> Command.Call.Clean
["compile"] -> Command.Call.Compile
("db" : dbArgs) -> Command.Call.Db dbArgs
@ -57,6 +62,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
case commandCall of
Command.Call.New projectName -> runCommand $ createNewProject projectName
Command.Call.Start -> runCommand start
Command.Call.StartDb -> runCommand Command.Start.Db.start
Command.Call.Clean -> runCommand clean
Command.Call.Compile -> runCommand compile
Command.Call.Db dbArgs -> dbCli dbArgs
@ -89,36 +95,40 @@ printUsage :: IO ()
printUsage =
putStrLn $
unlines
{- ORMOLU_DISABLE -}
[ title "USAGE",
" wasp <command> [command-args]",
"",
" wasp <command> [command-args]",
"",
title "COMMANDS",
title " GENERAL",
cmd " new <project-name> Creates new Wasp project.",
cmd " version Prints current version of CLI.",
cmd " waspls Run Wasp Language Server. Add --help to get more info.",
cmd " completion Prints help on bash completion.",
cmd " uninstall Removes Wasp from your system.",
cmd " new <project-name> Creates new Wasp project.",
cmd " version Prints current version of CLI.",
cmd " waspls Run Wasp Language Server. Add --help to get more info.",
cmd " completion Prints help on bash completion.",
cmd " uninstall Removes Wasp from your system.",
title " IN PROJECT",
cmd " start Runs Wasp app in development mode, watching for file changes.",
cmd " db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.",
cmd " clean Deletes all generated code and other cached artifacts. Wasp equivalent of 'have you tried closing and opening it again?'.",
cmd " build Generates full web app code, ready for deployment. Use when deploying or ejecting.",
cmd " deploy Deploys your Wasp app to cloud hosting providers.",
cmd " telemetry Prints telemetry status.",
cmd " deps Prints the dependencies that Wasp uses in your project.",
cmd " dockerfile Prints the contents of the Wasp generated Dockerfile.",
cmd " info Prints basic information about current Wasp project.",
"",
cmd " start Runs Wasp app in development mode, watching for file changes.",
cmd " start db Starts managed development database for you.",
cmd " db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.",
cmd $ " clean Deletes all generated code and other cached artifacts.",
" Wasp equivalent of 'have you tried closing and opening it again?'.",
cmd " build Generates full web app code, ready for deployment. Use when deploying or ejecting.",
cmd " deploy Deploys your Wasp app to cloud hosting providers.",
cmd " telemetry Prints telemetry status.",
cmd " deps Prints the dependencies that Wasp uses in your project.",
cmd " dockerfile Prints the contents of the Wasp generated Dockerfile.",
cmd " info Prints basic information about current Wasp project.",
"",
title "EXAMPLES",
" wasp new MyApp",
" wasp start",
" wasp db migrate-dev",
"",
Term.applyStyles [Term.Green] "Docs:" ++ " https://wasp-lang.dev/docs",
" wasp new MyApp",
" wasp start",
" wasp db migrate-dev",
"",
Term.applyStyles [Term.Green] "Docs:" ++ " https://wasp-lang.dev/docs",
Term.applyStyles [Term.Magenta] "Discord (chat):" ++ " https://discord.gg/rzdnErX",
Term.applyStyles [Term.Cyan] "Newsletter:" ++ " https://wasp-lang.dev/#signup"
Term.applyStyles [Term.Cyan] "Newsletter:" ++ " https://wasp-lang.dev/#signup"
]
{- ORMOLU_ENABLE -}
printVersion :: IO ()
printVersion = do
@ -128,44 +138,49 @@ printVersion = do
"",
"If you wish to install/switch to the latest version of Wasp, do:",
" curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s",
"or do",
" curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v x.y.z",
"if you want specific x.y.z version of Wasp.",
"",
"Check https://github.com/wasp-lang/wasp/releases for the list of valid versions, include the latest one."
"If you want specific x.y.z version of Wasp, do:",
" curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v x.y.z",
"",
"Check https://github.com/wasp-lang/wasp/releases for the list of valid versions, including the latest one."
]
-- TODO(matija): maybe extract to a separate module, e.g. DbCli.hs?
dbCli :: [String] -> IO ()
dbCli args = case args of
"migrate-dev" : optionalMigrateArgs -> runDbCommand $ Command.Db.Migrate.migrateDev optionalMigrateArgs
["studio"] -> runDbCommand studio
["reset"] -> runDbCommand Command.Db.Reset.reset
["studio"] -> runDbCommand Command.Db.Studio.studio
_ -> printDbUsage
printDbUsage :: IO ()
printDbUsage =
putStrLn $
unlines
{- ORMOLU_DISABLE -}
[ title "USAGE",
" wasp db <command> [command-args]",
"",
" wasp db <command> [command-args]",
"",
title "COMMANDS",
cmd
( " migrate-dev Ensures dev database corresponds to the current state of schema(entities):\n"
<> " - Generates a new migration if there are changes in the schema.\n"
<> " - Applies any pending migrations to the database either using the supplied migration name or asking for one.\n"
<> "\nOPTIONS:\n"
<> " --name [migration-name]\n"
<> " --create-only\n"
),
cmd " studio GUI for inspecting your database.",
"",
cmd " reset Drops all data and tables from development database and re-applies all migrations.",
cmd $ intercalate "\n" [
" migrate-dev Ensures dev database corresponds to the current state of schema(entities):",
" - Generates a new migration if there are changes in the schema.",
" - Applies any pending migrations to the database either using the",
" supplied migration name or asking for one.",
" OPTIONS:",
" --name [migration-name]",
" --create-only"
],
cmd " studio GUI for inspecting your database.",
"",
title "EXAMPLES",
" wasp db migrate-dev",
" wasp db migrate-dev --name \"Added User entity\"",
" wasp db migrate-dev --create-only",
" wasp db studio"
" wasp db migrate-dev",
" wasp db migrate-dev --name \"Added User entity\"",
" wasp db migrate-dev --create-only",
" wasp db studio"
]
{- ORMOLU_ENABLE -}
cmd :: String -> String
cmd = mapFirstWord (Term.applyStyles [Term.Yellow, Term.Bold])

View File

@ -21,10 +21,10 @@ import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Cli.Common as Common
import Wasp.Cli.Message (cliSendMessage)
import Wasp.CompileOptions (CompileOptions (..))
import qualified Wasp.Generator
import Wasp.Generator.Monad (GeneratorWarning (GeneratorNeedsMigrationWarning))
import Wasp.Lib (CompileError, CompileWarning)
import qualified Wasp.Lib
import qualified Wasp.Message as Msg
import Wasp.Project (CompileError, CompileWarning)
-- | Builds Wasp project that the current working directory is part of.
-- Does all the steps, from analysis to generation, and at the end writes generated code
@ -61,7 +61,7 @@ build = do
buildIO ::
Path' Abs (Dir Common.WaspProjectDir) ->
Path' Abs (Dir Wasp.Lib.ProjectRootDir) ->
Path' Abs (Dir Wasp.Generator.ProjectRootDir) ->
IO ([CompileWarning], [CompileError])
buildIO waspProjectDir buildDir = compileIOWithOptions options waspProjectDir buildDir
where

View File

@ -3,6 +3,7 @@ module Wasp.Cli.Command.Call where
data Call
= New String -- project name
| Start
| StartDb
| Clean
| Uninstall
| Compile

View File

@ -1,24 +1,25 @@
module Wasp.Cli.Command.Common
( findWaspProjectRootDirFromCwd,
findWaspProjectRoot,
readWaspCompileInfo,
throwIfExeIsNotAvailable,
)
where
import Control.Monad (unless, when)
import Control.Monad.Except (throwError)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Except
import qualified Control.Monad.Except as E
import Data.Maybe (fromJust)
import StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import System.Directory
( doesFileExist,
doesPathExist,
getCurrentDirectory,
)
import StrongPath.Operations
import System.Directory (doesFileExist, doesPathExist, findExecutable, getCurrentDirectory)
import qualified System.FilePath as FP
import Wasp.Cli.Command (Command, CommandError (..))
import Wasp.Cli.Common (dotWaspRootFileInWaspProjectDir)
import Wasp.Common (WaspProjectDir)
import qualified Wasp.Cli.Common as Cli.Common
import Wasp.Project (WaspProjectDir)
import Wasp.Util (ifM)
import qualified Wasp.Util.IO as IOUtil
findWaspProjectRoot :: Path' Abs (Dir ()) -> Command (Path' Abs (Dir WaspProjectDir))
findWaspProjectRoot currentDir = do
@ -45,3 +46,23 @@ findWaspProjectRootDirFromCwd :: Command (Path' Abs (Dir WaspProjectDir))
findWaspProjectRootDirFromCwd = do
absCurrentDir <- liftIO getCurrentDirectory
findWaspProjectRoot (fromJust $ SP.parseAbsDir absCurrentDir)
readWaspCompileInfo :: Path' Abs (Dir WaspProjectDir) -> IO String
readWaspCompileInfo waspDir =
ifM
(IOUtil.doesFileExist dotWaspInfoFile)
(IOUtil.readFile dotWaspInfoFile)
(return "No compile information found")
where
dotWaspInfoFile =
waspDir </> Cli.Common.dotWaspDirInWaspProjectDir
</> Cli.Common.generatedCodeDirInDotWaspDir
</> Cli.Common.dotWaspInfoFileInGeneratedCodeDir
throwIfExeIsNotAvailable :: String -> String -> Command ()
throwIfExeIsNotAvailable exeName explanationMsg = do
liftIO (findExecutable exeName) >>= \case
Just _ -> return ()
Nothing ->
E.throwError $
CommandError ("Couldn't find `" <> exeName <> "` executable") explanationMsg

View File

@ -6,6 +6,8 @@ module Wasp.Cli.Command.Compile
defaultCompileOptions,
printCompilationResult,
printWarningsAndErrorsIfAny,
analyze,
analyzeWithOptions,
)
where
@ -14,6 +16,7 @@ import Control.Monad.Except (throwError)
import Control.Monad.IO.Class (liftIO)
import Data.List (intercalate)
import StrongPath (Abs, Dir, Path', (</>))
import qualified Wasp.AppSpec as AS
import Wasp.Cli.Command (Command, CommandError (..))
import Wasp.Cli.Command.Common
( findWaspProjectRootDirFromCwd,
@ -21,11 +24,11 @@ import Wasp.Cli.Command.Common
import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Cli.Common as Common
import Wasp.Cli.Message (cliSendMessage)
import Wasp.Common (WaspProjectDir)
import Wasp.CompileOptions (CompileOptions (..))
import Wasp.Lib (CompileError, CompileWarning)
import qualified Wasp.Lib
import qualified Wasp.Generator
import qualified Wasp.Message as Msg
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
import qualified Wasp.Project
-- | Same like 'compileWithOptions', but with default compile options.
compile :: Command [CompileWarning]
@ -98,18 +101,18 @@ formatErrorOrWarningMessages = intercalate "\n" . map ("- " ++)
-- in given outDir directory.
compileIO ::
Path' Abs (Dir WaspProjectDir) ->
Path' Abs (Dir Wasp.Lib.ProjectRootDir) ->
Path' Abs (Dir Wasp.Generator.ProjectRootDir) ->
IO ([CompileWarning], [CompileError])
compileIO waspProjectDir outDir =
compileIOWithOptions (defaultCompileOptions waspProjectDir) waspProjectDir outDir
compileIOWithOptions ::
CompileOptions ->
Path' Abs (Dir Common.WaspProjectDir) ->
Path' Abs (Dir Wasp.Lib.ProjectRootDir) ->
Path' Abs (Dir WaspProjectDir) ->
Path' Abs (Dir Wasp.Generator.ProjectRootDir) ->
IO ([CompileWarning], [CompileError])
compileIOWithOptions options waspProjectDir outDir =
Wasp.Lib.compile waspProjectDir outDir options
Wasp.Project.compile waspProjectDir outDir options
defaultCompileOptions :: Path' Abs (Dir WaspProjectDir) -> CompileOptions
defaultCompileOptions waspProjectDir =
@ -121,3 +124,19 @@ defaultCompileOptions waspProjectDir =
sendMessage = cliSendMessage,
generatorWarningsFilter = id
}
analyze :: Path' Abs (Dir WaspProjectDir) -> Command AS.AppSpec
analyze waspProjectDir = do
analyzeWithOptions waspProjectDir $ defaultCompileOptions waspProjectDir
-- | Analyzes Wasp project that the current working directory is a part of and returns
-- AppSpec. So same like compilation, but it stops before any code generation.
-- Throws if there were any compilation errors.
analyzeWithOptions :: Path' Abs (Dir WaspProjectDir) -> CompileOptions -> Command AS.AppSpec
analyzeWithOptions waspProjectDir options = do
liftIO (Wasp.Project.analyzeWaspProject waspProjectDir options) >>= \case
Left errors ->
throwError $
CommandError "Analyzing wasp project failed" $
show (length errors) <> " errors found:\n" <> formatErrorOrWarningMessages errors
Right spec -> return spec

View File

@ -14,9 +14,8 @@ import qualified System.FilePath as FP
import Text.Printf (printf)
import Wasp.Analyzer.Parser (isValidWaspIdentifier)
import Wasp.Cli.Command (Command, CommandError (..))
import Wasp.Common (WaspProjectDir)
import qualified Wasp.Common as Common (WaspProjectDir)
import qualified Wasp.Data as Data
import Wasp.Project (WaspProjectDir)
import Wasp.Util (indent, kebabToCamelCase)
import qualified Wasp.Util.IO as IOUtil
import qualified Wasp.Util.Terminal as Term
@ -79,7 +78,7 @@ getAbsoluteWaspProjectDir (ProjectInfo projectName _) = do
"Failed to parse absolute path to wasp project dir: " ++ show err
-- Copies prepared files to the new project directory.
initializeProjectFromSkeleton :: Path' Abs (Dir Common.WaspProjectDir) -> IO ()
initializeProjectFromSkeleton :: Path' Abs (Dir WaspProjectDir) -> IO ()
initializeProjectFromSkeleton absWaspProjectDir = do
dataDir <- Data.getAbsDataDirPath
let absSkeletonDir = dataDir </> [reldir|Cli/templates/new|]

View File

@ -1,23 +1,13 @@
module Wasp.Cli.Command.Db
( runDbCommand,
studio,
)
where
import Control.Concurrent (newChan)
import Control.Concurrent.Async (concurrently)
import Control.Monad.IO.Class (liftIO)
import StrongPath ((</>))
import Wasp.Cli.Command (Command, runCommand)
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd)
import Wasp.Cli.Command.Compile (compileWithOptions, defaultCompileOptions)
import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Cli.Common as Common
import Wasp.CompileOptions (CompileOptions (generatorWarningsFilter))
import Wasp.Generator.DbGenerator.Jobs (runStudio)
import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed)
import Wasp.Generator.Monad (GeneratorWarning (GeneratorNeedsMigrationWarning))
import qualified Wasp.Message as Msg
runDbCommand :: Command a -> IO ()
runDbCommand = runCommand . makeDbCommand
@ -44,17 +34,3 @@ makeDbCommand cmd = do
_ -> True
)
}
-- TODO(matija): should we extract this into a separate file, like we did for migrate?
studio :: Command ()
studio = do
waspProjectDir <- findWaspProjectRootDirFromCwd
let genProjectDir =
waspProjectDir </> Common.dotWaspDirInWaspProjectDir
</> Common.generatedCodeDirInDotWaspDir
cliSendMessageC $ Msg.Start "Running studio..."
chan <- liftIO newChan
_ <- liftIO $ concurrently (readJobMessagesAndPrintThemPrefixed chan) (runStudio genProjectDir chan)
error "This should never happen, studio should never stop."

View File

@ -13,12 +13,11 @@ import Wasp.Cli.Command.Common
)
import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Cli.Common as Cli.Common
import Wasp.Common (DbMigrationsDir)
import qualified Wasp.Common
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.DbGenerator.Common (MigrateArgs (..), defaultMigrateArgs)
import qualified Wasp.Generator.DbGenerator.Operations as DbOps
import qualified Wasp.Message as Msg
import Wasp.Project.Db.Migrations (DbMigrationsDir, dbMigrationsDirInWaspProjectDir)
-- | NOTE(shayne): Performs database schema migration (based on current schema) in the generated project.
-- This assumes the wasp project migrations dir was copied from wasp source project by a previous compile.
@ -26,14 +25,13 @@ import qualified Wasp.Message as Msg
migrateDev :: [String] -> Command ()
migrateDev optionalMigrateArgs = do
waspProjectDir <- findWaspProjectRootDirFromCwd
let waspDbMigrationsDir = waspProjectDir </> Wasp.Common.dbMigrationsDirInWaspProjectDir
let waspDbMigrationsDir = waspProjectDir </> dbMigrationsDirInWaspProjectDir
let projectRootDir =
waspProjectDir
</> Cli.Common.dotWaspDirInWaspProjectDir
</> Cli.Common.generatedCodeDirInDotWaspDir
migrateDatabase optionalMigrateArgs projectRootDir waspDbMigrationsDir
generatePrismaClients projectRootDir
migrateDatabase :: [String] -> Path' Abs (Dir ProjectRootDir) -> Path' Abs (Dir DbMigrationsDir) -> Command ()
migrateDatabase optionalMigrateArgs projectRootDir dbMigrationsDir = do
@ -46,14 +44,6 @@ migrateDatabase optionalMigrateArgs projectRootDir dbMigrationsDir = do
migrateArgs <- liftEither $ parseMigrateArgs optionalMigrateArgs
ExceptT $ DbOps.migrateDevAndCopyToSource dbMigrationsDir projectRootDir migrateArgs
generatePrismaClients :: Path' Abs (Dir ProjectRootDir) -> Command ()
generatePrismaClients projectRootDir = do
cliSendMessageC $ Msg.Start "Generating prisma clients..."
generatePrismaClientsResult <- liftIO $ DbOps.generatePrismaClients projectRootDir
case generatePrismaClientsResult of
Left err -> throwError $ CommandError "Could not generate Prisma clients" err
Right () -> cliSendMessageC $ Msg.Success "Prisma clients successfully generated."
-- | Basic parsing of db-migrate args. In the future, we could use a smarter parser
-- for this (and all other CLI arg parsing).
parseMigrateArgs :: [String] -> Either String MigrateArgs

View File

@ -0,0 +1,25 @@
module Wasp.Cli.Command.Db.Reset
( reset,
)
where
import Control.Monad.IO.Class (liftIO)
import StrongPath ((</>))
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd)
import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Cli.Common as Common
import Wasp.Generator.DbGenerator.Operations (dbReset)
import qualified Wasp.Message as Msg
reset :: Command ()
reset = do
waspProjectDir <- findWaspProjectRootDirFromCwd
let genProjectDir =
waspProjectDir </> Common.dotWaspDirInWaspProjectDir </> Common.generatedCodeDirInDotWaspDir
cliSendMessageC $ Msg.Start "Resetting the database..."
liftIO (dbReset genProjectDir) >>= \case
Left errorMsg -> cliSendMessageC $ Msg.Failure "Database reset failed" errorMsg
Right () -> cliSendMessageC $ Msg.Success "Database reset successfully!"

View File

@ -0,0 +1,29 @@
module Wasp.Cli.Command.Db.Studio
( studio,
)
where
import Control.Concurrent (newChan)
import Control.Concurrent.Async (concurrently)
import Control.Monad.IO.Class (liftIO)
import StrongPath ((</>))
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd)
import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Cli.Common as Common
import Wasp.Generator.DbGenerator.Jobs (runStudio)
import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed)
import qualified Wasp.Message as Msg
studio :: Command ()
studio = do
waspProjectDir <- findWaspProjectRootDirFromCwd
let genProjectDir =
waspProjectDir </> Common.dotWaspDirInWaspProjectDir </> Common.generatedCodeDirInDotWaspDir
cliSendMessageC $ Msg.Start "Running studio..."
chan <- liftIO newChan
_ <- liftIO $ readJobMessagesAndPrintThemPrefixed chan `concurrently` runStudio genProjectDir chan
error "This should never happen, studio should never stop."

View File

@ -7,13 +7,15 @@ import Control.Monad.IO.Class (liftIO)
import System.Environment (getExecutablePath)
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd)
import qualified Wasp.Lib as Lib
import qualified Wasp.Project.Deployment
deploy :: [String] -> Command ()
deploy cmdArgs = do
waspProjectDir <- findWaspProjectRootDirFromCwd
liftIO $ do
-- `getExecutablePath` has some caveats: https://frasertweedale.github.io/blog-fp/posts/2022-05-10-improved-executable-path-queries.html
-- Once we upgrade to GHC 9.4 we should change to `executablePath`, but this should be ok for our purposes.
-- `getExecutablePath` has some caveats:
-- https://frasertweedale.github.io/blog-fp/posts/2022-05-10-improved-executable-path-queries.html
-- Once we upgrade to GHC 9.4 we should change to `executablePath`, but this should be ok for
-- our purposes.
waspExePath <- getExecutablePath
Lib.deploy waspExePath waspProjectDir cmdArgs
Wasp.Project.Deployment.deploy waspExePath waspProjectDir cmdArgs

View File

@ -14,7 +14,7 @@ import Wasp.Cli.Terminal (title)
import qualified Wasp.Generator.NpmDependencies as N
import qualified Wasp.Generator.ServerGenerator as ServerGenerator
import qualified Wasp.Generator.WebAppGenerator as WebAppGenerator
import Wasp.Lib (analyzeWaspProject)
import Wasp.Project (analyzeWaspProject)
import qualified Wasp.Util.Terminal as Term
deps :: Command ()

View File

@ -9,7 +9,7 @@ import qualified Data.Text.IO as T.IO
import Wasp.Cli.Command (Command, CommandError (..))
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd)
import Wasp.Cli.Command.Compile (defaultCompileOptions)
import Wasp.Lib (compileAndRenderDockerfile)
import Wasp.Project (compileAndRenderDockerfile)
printDockerfile :: Command ()
printDockerfile = do

View File

@ -5,45 +5,43 @@ module Wasp.Cli.Command.Info
)
where
import Control.Arrow
import Control.Monad.Except
import Control.Arrow ()
import Control.Monad.Except (MonadIO (liftIO))
import StrongPath (Abs, Dir, Path', fromRelFile)
import StrongPath.Operations
import StrongPath.Operations ()
import System.Directory (getFileSize)
import qualified Wasp.Analyzer as Analyzer
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.Core.Decl as AS (Decl, takeDecls)
import qualified Wasp.AppSpec.Valid as ASV
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd)
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd, readWaspCompileInfo)
import Wasp.Cli.Command.Compile (analyze)
import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Cli.Common as Cli.Common
import Wasp.Cli.Command.Start.Db (getDbSystem)
import Wasp.Cli.Terminal (title)
import Wasp.Common (WaspProjectDir)
import Wasp.Error (showCompilerErrorForTerminal)
import Wasp.Lib (findWaspFile)
import qualified Wasp.Message as Msg
import Wasp.Util (ifM)
import Wasp.Project (WaspProjectDir)
import qualified Wasp.Util.IO as IOUtil
import qualified Wasp.Util.Terminal as Term
info :: Command ()
info = do
waspDir <- findWaspProjectRootDirFromCwd
compileInfo <- liftIO $ readCompileInformation waspDir
compileInfo <- liftIO $ readWaspCompileInfo waspDir
projectSize <- liftIO $ readDirectorySizeMB waspDir
declsOrError <- liftIO $ parseWaspFile waspDir
case declsOrError of
Left err -> cliSendMessageC $ Msg.Failure "Info failed" err
Right decls -> do
cliSendMessageC $
Msg.Info $
unlines
[ "",
title "Project information",
printInfo "Name" (fst $ head $ AS.takeDecls @AS.App.App decls),
printInfo "Last compile" compileInfo,
printInfo "Project size" projectSize
]
appSpec <- analyze waspDir
let (appName, app) = ASV.getApp appSpec
cliSendMessageC $
Msg.Info $
unlines
[ "",
title "Project information",
printInfo "Name" appName,
printInfo "Database system" $ show $ getDbSystem app,
printInfo "Last compile" compileInfo,
printInfo "Project dir size" projectSize
]
printInfo :: String -> String -> String
printInfo key value = Term.applyStyles [Term.Cyan] key ++ ": " <> Term.applyStyles [Term.White] value
@ -52,26 +50,3 @@ readDirectorySizeMB :: Path' Abs (Dir WaspProjectDir) -> IO String
readDirectorySizeMB path = (++ " MB") . show . (`div` 1000000) . sum <$> allFileSizes
where
allFileSizes = IOUtil.listDirectoryDeep path >>= mapM (getFileSize . fromRelFile)
readCompileInformation :: Path' Abs (Dir WaspProjectDir) -> IO String
readCompileInformation waspDir =
ifM
(IOUtil.doesFileExist dotWaspInfoFile)
(IOUtil.readFile dotWaspInfoFile)
(return "No compile information found")
where
dotWaspInfoFile =
waspDir </> Cli.Common.dotWaspDirInWaspProjectDir
</> Cli.Common.generatedCodeDirInDotWaspDir
</> Cli.Common.dotWaspInfoFileInGeneratedCodeDir
parseWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String [AS.Decl])
parseWaspFile waspDir = runExceptT $ do
waspFile <- ExceptT $ findWaspFile waspDir
waspStr <- liftIO $ IOUtil.readFile waspFile
liftEither $ left (annotateErrorForCli waspFile waspStr) $ Analyzer.analyze waspStr
where
annotateErrorForCli waspFile waspStr =
("Couldn't parse .wasp file:\n" ++)
. showCompilerErrorForTerminal (waspFile, waspStr)
. Analyzer.getErrorMessageAndCtx

View File

@ -15,9 +15,9 @@ import Wasp.Cli.Command.Compile (compile, printWarningsAndErrorsIfAny)
import Wasp.Cli.Command.Message (cliSendMessageC)
import Wasp.Cli.Command.Watch (watch)
import qualified Wasp.Cli.Common as Common
import Wasp.Lib (CompileError, CompileWarning)
import qualified Wasp.Lib
import qualified Wasp.Generator
import qualified Wasp.Message as Msg
import Wasp.Project (CompileError, CompileWarning)
-- | Does initial compile of wasp code and then runs the generated project.
-- It also listens for any file changes and recompiles and restarts generated project accordingly.
@ -41,7 +41,7 @@ start = do
-- 'watch') once jobs from 'start' quiet down a bit.
ongoingCompilationResultMVar <- newMVar (warnings, [])
let watchWaspProjectSource = watch waspRoot outDir ongoingCompilationResultMVar
let startGeneratedWebApp = Wasp.Lib.start outDir (onJobsQuietDown ongoingCompilationResultMVar)
let startGeneratedWebApp = Wasp.Generator.start outDir (onJobsQuietDown ongoingCompilationResultMVar)
-- In parallel:
-- 1. watch for any changes in the Wasp project, be it users wasp code or users JS/HTML/...
-- code. On any change, Wasp is recompiled (and generated code is re-generated).

View File

@ -0,0 +1,175 @@
module Wasp.Cli.Command.Start.Db
( start,
getDbSystem,
waspDevDbDockerVolumePrefix,
)
where
import Control.Monad (when)
import qualified Control.Monad.Except as E
import Control.Monad.IO.Class (liftIO)
import Data.Maybe (fromMaybe, isJust)
import StrongPath (Abs, Dir, File', Path', Rel, fromRelFile)
import System.Environment (lookupEnv)
import System.Process (callCommand)
import Text.Printf (printf)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Db as AS.App.Db
import qualified Wasp.AppSpec.Valid as ASV
import Wasp.Cli.Command (Command, CommandError (CommandError))
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd, throwIfExeIsNotAvailable)
import Wasp.Cli.Command.Compile (analyze)
import Wasp.Cli.Command.Message (cliSendMessageC)
import Wasp.Cli.Common (WaspProjectDir)
import qualified Wasp.Message as Msg
import Wasp.Project.Db (databaseUrlEnvVarName)
import Wasp.Project.Db.Dev (makeDevDbUniqueId)
import qualified Wasp.Project.Db.Dev.Postgres as Dev.Postgres
import Wasp.Project.Env (dotEnvServer)
import Wasp.Util (whenM)
import qualified Wasp.Util.Network.Socket as Socket
-- | Starts a "managed" dev database, where "managed" means that
-- Wasp creates it and connects the Wasp app with it.
-- Wasp is smart while doing this so it checks which database is specified
-- in Wasp configuration and spins up a database of appropriate type.
start :: Command ()
start = do
waspProjectDir <- findWaspProjectRootDirFromCwd
appSpec <- analyze waspProjectDir
throwIfCustomDbAlreadyInUse appSpec
let (appName, app) = ASV.getApp appSpec
case getDbSystem app of
AS.App.Db.SQLite -> noteSQLiteDoesntNeedStart
AS.App.Db.PostgreSQL -> startPostgreDevDb waspProjectDir appName
where
noteSQLiteDoesntNeedStart =
cliSendMessageC . Msg.Info $
"Nothing to do! You are all good, you are using SQLite which doesn't need to be started."
throwIfCustomDbAlreadyInUse :: AS.AppSpec -> Command ()
throwIfCustomDbAlreadyInUse spec = do
throwIfDbUrlInEnv
throwIfDbUrlInServerDotEnv spec
where
throwIfDbUrlInEnv :: Command ()
throwIfDbUrlInEnv = do
dbUrl <- liftIO $ lookupEnv databaseUrlEnvVarName
when (isJust dbUrl) $
throwCustomDbAlreadyInUseError
( "Wasp has detected existing " <> databaseUrlEnvVarName <> " var in your environment.\n"
<> "To have Wasp run the dev database for you, make sure you remove that env var first."
)
throwIfDbUrlInServerDotEnv :: AS.AppSpec -> Command ()
throwIfDbUrlInServerDotEnv appSpec =
when (isThereDbUrlInServerDotEnv appSpec) $
throwCustomDbAlreadyInUseError
( printf
( "Wasp has detected that you have defined %s env var in your %s file.\n"
<> "To have Wasp run the dev database for you, make sure you remove that env var first."
)
databaseUrlEnvVarName
(fromRelFile (dotEnvServer :: Path' (Rel WaspProjectDir) File'))
)
where
isThereDbUrlInServerDotEnv = any ((== databaseUrlEnvVarName) . fst) . AS.devEnvVarsServer
throwCustomDbAlreadyInUseError :: String -> Command ()
throwCustomDbAlreadyInUseError msg =
E.throwError $ CommandError "You are using custom database already" msg
getDbSystem :: AS.App.App -> AS.App.Db.DbSystem
getDbSystem app =
fromMaybe AS.App.Db.SQLite (AS.App.db app >>= AS.App.Db.system)
startPostgreDevDb :: Path' Abs (Dir WaspProjectDir) -> String -> Command ()
startPostgreDevDb waspProjectDir appName = do
throwIfExeIsNotAvailable
"docker"
"To run PostgreSQL dev database, Wasp needs `docker` installed and in PATH."
throwIfDevDbPortIsAlreadyInUse
cliSendMessageC . Msg.Info $
unlines
[ "✨ Starting a PostgreSQL dev database (based on your Wasp config) ✨",
"",
"Additional info:",
" Connection URL, in case you might want to connect with external tools:",
" " <> connectionUrl,
" Database data is persisted in a docker volume with the following name"
<> " (useful to know if you will want to delete it at some point):",
" " <> dockerVolumeName
]
-- NOTE: POSTGRES_PASSWORD, POSTGRES_USER, POSTGRES_DB below are really used by the docker image
-- only when initializing the database -> if it already exists, they will be ignored.
-- This is how the postgres Docker image works.
let command =
printf
( unwords
[ "docker run",
"--name %s",
"--rm",
"--publish %d:5432",
"-v %s:/var/lib/postgresql/data",
"--env POSTGRES_PASSWORD=%s",
"--env POSTGRES_USER=%s",
"--env POSTGRES_DB=%s",
"postgres"
]
)
dockerContainerName
Dev.Postgres.defaultDevPort
dockerVolumeName
Dev.Postgres.defaultDevPass
Dev.Postgres.defaultDevUser
dbName
liftIO $ callCommand command
where
dockerVolumeName = makeWaspDevDbDockerVolumeName waspProjectDir appName
dockerContainerName = makeWaspDevDbDockerContainerName waspProjectDir appName
dbName = Dev.Postgres.makeDevDbName waspProjectDir appName
connectionUrl = Dev.Postgres.makeDevConnectionUrl waspProjectDir appName
throwIfDevDbPortIsAlreadyInUse :: Command ()
throwIfDevDbPortIsAlreadyInUse = do
-- I am checking both conditions because of Docker having virtual network on Mac which
-- always gives precedence to native ports so checking only if we can open the port is
-- not enough because we can open it even if Docker container is already bound to that port.
whenM (liftIO $ Socket.checkIfPortIsInUse devDbSocketAddress) throwPortAlreadyInUseError
whenM (liftIO $ Socket.checkIfPortIsAcceptingConnections devDbSocketAddress) throwPortAlreadyInUseError
where
devDbSocketAddress = Socket.makeLocalHostSocketAddress $ fromIntegral Dev.Postgres.defaultDevPort
throwPortAlreadyInUseError =
E.throwError $
CommandError
"Port already in use"
( printf
"Wasp can't run PostgreSQL dev database for you since port %d is already in use."
Dev.Postgres.defaultDevPort
)
-- | Docker volume name unique for the Wasp project with specified path and name.
makeWaspDevDbDockerVolumeName :: Path' Abs (Dir WaspProjectDir) -> String -> String
makeWaspDevDbDockerVolumeName waspProjectDir appName =
take maxDockerVolumeNameLength $
waspDevDbDockerVolumePrefix <> "-" <> makeDevDbUniqueId waspProjectDir appName
waspDevDbDockerVolumePrefix :: String
waspDevDbDockerVolumePrefix = "wasp-dev-db"
maxDockerVolumeNameLength :: Int
maxDockerVolumeNameLength = 255
-- | Docker container name unique for the Wasp project with specified path and name.
makeWaspDevDbDockerContainerName :: Path' Abs (Dir WaspProjectDir) -> String -> String
makeWaspDevDbDockerContainerName waspProjectDir appName =
take maxDockerContainerNameLength $
waspDevDbDockerVolumePrefix <> "-" <> makeDevDbUniqueId waspProjectDir appName
maxDockerContainerNameLength :: Int
maxDockerContainerNameLength = 63

View File

@ -9,6 +9,7 @@ import StrongPath (fromAbsDir, fromAbsFile, (</>))
import System.Exit (die)
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.Message (cliSendMessageC)
import Wasp.Cli.Command.Start.Db (waspDevDbDockerVolumePrefix)
import Wasp.Cli.FileSystem
( getHomeDir,
getUserCacheDir,
@ -24,7 +25,12 @@ uninstall :: Command ()
uninstall = do
cliSendMessageC $ Msg.Start "Uninstalling Wasp ..."
liftIO removeWaspFiles
cliSendMessageC $ Msg.Success "Uninstalled Wasp"
cliSendMessageC $ Msg.Success "Uninstalled Wasp."
cliSendMessageC $
Msg.Info $
"If you have used Wasp to run dev database for you, you might want to make sure you also"
<> " deleted all the docker volumes it might have created."
<> (" You can easily list them by doing `docker volume ls | grep " <> waspDevDbDockerVolumePrefix <> "`.")
removeWaspFiles :: IO ()
removeWaspFiles = do

View File

@ -17,9 +17,9 @@ import qualified System.FilePath as FP
import Wasp.Cli.Command.Compile (compileIO, printCompilationResult)
import qualified Wasp.Cli.Common as Common
import Wasp.Cli.Message (cliSendMessage)
import Wasp.Lib (CompileError, CompileWarning)
import qualified Wasp.Lib
import qualified Wasp.Generator.Common as Wasp.Generator
import qualified Wasp.Message as Msg
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
-- TODO: Idea: Read .gitignore file, and ignore everything from it. This will then also cover the
-- .wasp dir, and users can easily add any custom stuff they want ignored. But, we also have to
@ -32,8 +32,8 @@ import qualified Wasp.Message as Msg
-- (warnings, errors) of the latest (re)compile whenever it happens. If there is already
-- something in the MVar, it will get overwritten.
watch ::
Path' Abs (Dir Common.WaspProjectDir) ->
Path' Abs (Dir Wasp.Lib.ProjectRootDir) ->
Path' Abs (Dir WaspProjectDir) ->
Path' Abs (Dir Wasp.Generator.ProjectRootDir) ->
MVar ([CompileWarning], [CompileError]) ->
IO ()
watch waspProjectDir outDir ongoingCompilationResultMVar = FSN.withManager $ \mgr -> do

View File

@ -18,8 +18,8 @@ where
import StrongPath (Dir, File', Path', Rel, reldir, relfile)
import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir)
import Wasp.Common (WaspProjectDir)
import qualified Wasp.Generator.Common
import Wasp.Project (WaspProjectDir)
import qualified Wasp.Util.Terminal as Term
data DotWaspDir -- Here we put everything that wasp generates.

View File

@ -14,7 +14,7 @@ const config = {
all: {
env,
port: parseInt(process.env.PORT) || 3001,
databaseUrl: process.env.{= databaseUrlEnvVar =},
databaseUrl: process.env.{= databaseUrlEnvVarName =},
frontendUrl: undefined,
allowedCORSOrigins: [],
{=# isAuthEnabled =}

View File

@ -12,4 +12,4 @@ constraints:
prune-juice ==0.7
-- See cabal.project.
index-state: 2022-03-22T14:16:26Z
index-state: 2023-03-09T09:15:49Z

View File

@ -89,7 +89,14 @@ runGoldenTest goldenTest = do
isTestOutputFileTestable :: FilePath -> Bool
isTestOutputFileTestable fp =
takeFileName fp `notElem` [".waspinfo", "node_modules", "dev.db", "dev.db-journal", "package-lock.json", ".gitignore"]
takeFileName fp
`notElem` [ ".waspinfo",
"node_modules",
"dev.db",
"dev.db-journal",
"package-lock.json",
".gitignore"
]
writeFileManifest :: String -> [FilePath] -> FilePath -> IO ()
writeFileManifest baseAbsFp filePaths manifestAbsFp = do

View File

@ -13,6 +13,7 @@ import ShellCommands
waspCliNew,
)
import Util ((<++>))
import Wasp.Project.Db (databaseUrlEnvVarName)
waspComplexTest :: GoldenTest
waspComplexTest = do
@ -120,7 +121,15 @@ addServerEnvFile = do
envFileContents =
unlines
[ "GOOGLE_CLIENT_ID=google_client_id",
"GOOGLE_CLIENT_SECRET=google_client_secret"
"GOOGLE_CLIENT_SECRET=google_client_secret",
-- NOTE: Since we are using PSQL in this test, if we don't set custom
-- database url in server/.env, Wasp will set its own, for managed dev db.
-- That is problematic because Wasp's db url depends on project's abs path,
-- which is not something we have constant during e2e tests, it depends
-- on the location where the tests are being run.
-- Therefore, we make sure to set custom database url here, to avoid .env
-- changing between different machines / setups.
databaseUrlEnvVarName <> "=" <> "mock-database-url"
]
addGoogleAuth :: ShellCommandBuilder [ShellCommand]

View File

@ -2,13 +2,17 @@ module Tests.WaspJobTest (waspJob) where
import GoldenTest (GoldenTest, makeGoldenTest)
import ShellCommands
( appendToWaspFile,
( ShellCommand,
ShellCommandBuilder,
appendToWaspFile,
cdIntoCurrentProject,
createFile,
setDbToPSQL,
waspCliCompile,
waspCliNew,
)
import Util ((<++>))
import Wasp.Project.Db (databaseUrlEnvVarName)
waspJob :: GoldenTest
waspJob = do
@ -31,6 +35,25 @@ waspJob = do
cdIntoCurrentProject,
setDbToPSQL,
appendToWaspFile jobDecl,
createFile jobFile "./src/server/jobs" "bar.js",
waspCliCompile
createFile jobFile "./src/server/jobs" "bar.js"
]
<++> addServerEnvFile
<++> sequence
[ waspCliCompile
]
addServerEnvFile :: ShellCommandBuilder [ShellCommand]
addServerEnvFile = do
sequence [createFile envFileContents "./" ".env.server"]
where
envFileContents =
unlines
[ -- NOTE: Since we are using PSQL in this test, if we don't set custom
-- database url in server/.env, Wasp will set its own, for managed dev db.
-- That is problematic because Wasp's db url depends on project's abs path,
-- which is not something we have constant during e2e tests, it depends
-- on the location where the tests are being run.
-- Therefore, we make sure to set custom database url here, to avoid .env
-- changing between different machines / setups.
databaseUrlEnvVarName <> "=" <> "mock-database-url"
]

View File

@ -1,7 +1,7 @@
app waspBuild {
db: { system: PostgreSQL },
wasp: {
version: "^0.9.0"
version: "^0.9.1"
},
title: "waspBuild"
}

View File

@ -3,6 +3,7 @@ waspCompile/.wasp/out/.waspchecksums
waspCompile/.wasp/out/Dockerfile
waspCompile/.wasp/out/db/schema.prisma
waspCompile/.wasp/out/installedFullStackNpmDependencies.json
waspCompile/.wasp/out/server/.env
waspCompile/.wasp/out/server/.npmrc
waspCompile/.wasp/out/server/README.md
waspCompile/.wasp/out/server/nodemon.json
@ -32,6 +33,7 @@ waspCompile/.wasp/out/server/src/types/index.ts
waspCompile/.wasp/out/server/src/universal/url.ts
waspCompile/.wasp/out/server/src/utils.js
waspCompile/.wasp/out/server/tsconfig.json
waspCompile/.wasp/out/web-app/.env
waspCompile/.wasp/out/web-app/.npmrc
waspCompile/.wasp/out/web-app/README.md
waspCompile/.wasp/out/web-app/index.html

View File

@ -20,6 +20,13 @@
],
"93f3b154b04fce7819e24aeb1691cd1c78f731f41a2f9e0213d54ef783f2bc38"
],
[
[
"file",
"server/.env"
],
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
],
[
[
"file",
@ -230,6 +237,13 @@
],
"f2632965c1e3678fcc0e63b83d7e33fea1a9008ef5fd5a2f5e7bf278337c3e02"
],
[
[
"file",
"web-app/.env"
],
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
],
[
[
"file",

View File

@ -1,6 +1,6 @@
app waspCompile {
wasp: {
version: "^0.9.0"
version: "^0.9.1"
},
title: "waspCompile"
}

View File

@ -61,6 +61,7 @@ waspComplexTest/.wasp/out/server/src/types/index.ts
waspComplexTest/.wasp/out/server/src/universal/url.ts
waspComplexTest/.wasp/out/server/src/utils.js
waspComplexTest/.wasp/out/server/tsconfig.json
waspComplexTest/.wasp/out/web-app/.env
waspComplexTest/.wasp/out/web-app/.npmrc
waspComplexTest/.wasp/out/web-app/README.md
waspComplexTest/.wasp/out/web-app/index.html

View File

@ -1,3 +1,4 @@
GOOGLE_CLIENT_ID=google_client_id
GOOGLE_CLIENT_SECRET=google_client_secret
DATABASE_URL=mock-database-url

View File

@ -25,7 +25,7 @@
"file",
"server/.env"
],
"7868f4dd9dc579bb21167f4e4cd5caa2960bf44ebe7d3f9f27a2f9380491d31f"
"368bf0f88f43e6cc3083d535f348905e51a1d3747abb479749dc250253a0c042"
],
[
[
@ -412,6 +412,13 @@
],
"f2632965c1e3678fcc0e63b83d7e33fea1a9008ef5fd5a2f5e7bf278337c3e02"
],
[
[
"file",
"web-app/.env"
],
"368bf0f88f43e6cc3083d535f348905e51a1d3747abb479749dc250253a0c042"
],
[
[
"file",

View File

@ -1,3 +1,3 @@
GOOGLE_CLIENT_ID=google_client_id
GOOGLE_CLIENT_SECRET=google_client_secret
GOOGLE_CLIENT_ID="google_client_id"
GOOGLE_CLIENT_SECRET="google_client_secret"
DATABASE_URL="mock-database-url"

View File

@ -0,0 +1,3 @@
GOOGLE_CLIENT_ID="google_client_id"
GOOGLE_CLIENT_SECRET="google_client_secret"
DATABASE_URL="mock-database-url"

View File

@ -1,7 +1,7 @@
app waspComplexTest {
db: { system: PostgreSQL },
wasp: {
version: "^0.9.0"
version: "^0.9.1"
},
auth: {
userEntity: User,

View File

@ -1,8 +1,10 @@
waspJob/.env.server
waspJob/.wasp/out/.dockerignore
waspJob/.wasp/out/.waspchecksums
waspJob/.wasp/out/Dockerfile
waspJob/.wasp/out/db/schema.prisma
waspJob/.wasp/out/installedFullStackNpmDependencies.json
waspJob/.wasp/out/server/.env
waspJob/.wasp/out/server/.npmrc
waspJob/.wasp/out/server/README.md
waspJob/.wasp/out/server/nodemon.json
@ -34,6 +36,7 @@ waspJob/.wasp/out/server/src/types/index.ts
waspJob/.wasp/out/server/src/universal/url.ts
waspJob/.wasp/out/server/src/utils.js
waspJob/.wasp/out/server/tsconfig.json
waspJob/.wasp/out/web-app/.env
waspJob/.wasp/out/web-app/.npmrc
waspJob/.wasp/out/web-app/README.md
waspJob/.wasp/out/web-app/index.html

View File

@ -0,0 +1,2 @@
DATABASE_URL=mock-database-url

View File

@ -20,6 +20,13 @@
],
"6f7b1b109e332bad9eb3cda4a2caf4963f4918c91b546c06fa42d8986c0b94a2"
],
[
[
"file",
"server/.env"
],
"b267ea48e5ff257c153b4f84102104eec7af8a46827aae6e76fc42c83934cfc0"
],
[
[
"file",
@ -244,6 +251,13 @@
],
"f2632965c1e3678fcc0e63b83d7e33fea1a9008ef5fd5a2f5e7bf278337c3e02"
],
[
[
"file",
"web-app/.env"
],
"b267ea48e5ff257c153b4f84102104eec7af8a46827aae6e76fc42c83934cfc0"
],
[
[
"file",

View File

@ -0,0 +1 @@
DATABASE_URL="mock-database-url"

View File

@ -0,0 +1 @@
DATABASE_URL="mock-database-url"

View File

@ -1,7 +1,7 @@
app waspJob {
db: { system: PostgreSQL },
wasp: {
version: "^0.9.0"
version: "^0.9.1"
},
title: "waspJob"
}

View File

@ -8,6 +8,7 @@ waspMigrate/.wasp/out/db/schema.prisma
waspMigrate/.wasp/out/db/schema.prisma.wasp-generate-checksum
waspMigrate/.wasp/out/db/schema.prisma.wasp-last-db-concurrence-checksum
waspMigrate/.wasp/out/installedFullStackNpmDependencies.json
waspMigrate/.wasp/out/server/.env
waspMigrate/.wasp/out/server/.npmrc
waspMigrate/.wasp/out/server/README.md
waspMigrate/.wasp/out/server/nodemon.json
@ -37,6 +38,7 @@ waspMigrate/.wasp/out/server/src/types/index.ts
waspMigrate/.wasp/out/server/src/universal/url.ts
waspMigrate/.wasp/out/server/src/utils.js
waspMigrate/.wasp/out/server/tsconfig.json
waspMigrate/.wasp/out/web-app/.env
waspMigrate/.wasp/out/web-app/.npmrc
waspMigrate/.wasp/out/web-app/README.md
waspMigrate/.wasp/out/web-app/index.html

View File

@ -20,6 +20,13 @@
],
"8d017edd849a861ae086850270a9f817bb4b75d9ee9ac27c08b0e9c29a16f6fe"
],
[
[
"file",
"server/.env"
],
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
],
[
[
"file",
@ -230,6 +237,13 @@
],
"f2632965c1e3678fcc0e63b83d7e33fea1a9008ef5fd5a2f5e7bf278337c3e02"
],
[
[
"file",
"web-app/.env"
],
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
],
[
[
"file",

View File

@ -1,6 +1,6 @@
app waspMigrate {
wasp: {
version: "^0.9.0"
version: "^0.9.1"
},
title: "waspMigrate"
}

View File

@ -1,6 +1,6 @@
app waspNew {
wasp: {
version: "^0.9.0"
version: "^0.9.1"
},
title: "waspNew"
}

View File

@ -1,6 +1,6 @@
app todoApp {
wasp: {
version: "^0.9.0"
version: "^0.9.1"
},
title: "ToDo App",
// head: [],

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.AppSpec
( AppSpec (..),
Decl,
@ -15,6 +17,7 @@ module Wasp.AppSpec
resolveRef,
doesConfigFileExist,
asAbsWaspProjectDirFile,
getApp,
)
where
@ -24,6 +27,7 @@ import Data.Text (Text)
import StrongPath (Abs, Dir, File', Path', Rel, (</>))
import Wasp.AppSpec.Action (Action)
import Wasp.AppSpec.Api (Api)
import Wasp.AppSpec.App (App)
import Wasp.AppSpec.ConfigFile (ConfigFileRelocator (..))
import Wasp.AppSpec.Core.Decl (Decl, IsDecl, takeDecls)
import Wasp.AppSpec.Core.Ref (Ref, refName)
@ -33,7 +37,9 @@ import Wasp.AppSpec.Job (Job)
import Wasp.AppSpec.Page (Page)
import Wasp.AppSpec.Query (Query)
import Wasp.AppSpec.Route (Route)
import Wasp.Common (DbMigrationsDir, WaspProjectDir)
import Wasp.Env (EnvVar)
import Wasp.Project.Common (WaspProjectDir)
import Wasp.Project.Db.Migrations (DbMigrationsDir)
-- | AppSpec is the main/central intermediate representation (IR) of the whole Wasp compiler,
-- describing the web app specification with all the details needed to generate it.
@ -52,19 +58,20 @@ data AppSpec = AppSpec
externalSharedFiles :: [ExternalCode.File],
-- | Absolute path to the directory in wasp project source that contains external code files.
migrationsDir :: Maybe (Path' Abs (Dir DbMigrationsDir)),
-- | Absolute path to the .env.server file in wasp project source. It contains env variables to be
-- provided to the server only during the development.
dotEnvServerFile :: Maybe (Path' Abs File'),
-- | Absolute path to the .env.client file in wasp project source. It contains env variables to be
-- provided to the client only during the development.
dotEnvClientFile :: Maybe (Path' Abs File'),
-- | Env variables to be provided to the server only during the development.
devEnvVarsServer :: [EnvVar],
-- | Env variables to be provided to the client only during the development.
devEnvVarsClient :: [EnvVar],
-- | If true, it means project is being compiled for production/deployment -> it is being "built".
-- If false, it means project is being compiled for development purposes (e.g. "wasp start").
isBuild :: Bool,
-- | The contents of the optional user Dockerfile found in the root of the wasp project source.
userDockerfileContents :: Maybe Text,
-- | A list of paths to any config files found (e.g., tailwind.config.cjs) and where to copy them.
configFiles :: [ConfigFileRelocator]
configFiles :: [ConfigFileRelocator],
-- | Connection URL for a database used during development. If provided, generated app will
-- make sure to use it when run in development mode.
devDatabaseUrl :: Maybe String
}
-- TODO: Make this return "Named" declarations?
@ -96,6 +103,11 @@ getRoutes = getDecls
getJobs :: AppSpec -> [(String, Job)]
getJobs = getDecls
getApp :: [Decl] -> Maybe (String, App)
getApp dcls = case takeDecls @App dcls of
[] -> Nothing
apps -> Just $ head apps
resolveRef :: (IsDecl d) => AppSpec -> Ref d -> (String, d)
resolveRef spec ref =
fromMaybe

View File

@ -4,8 +4,8 @@ module Wasp.AppSpec.ConfigFile
where
import StrongPath (File', Path', Rel)
import Wasp.Common (WaspProjectDir)
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Project.Common (WaspProjectDir)
-- | A type for establishing the mapping of where to copy config files from/to.
data ConfigFileRelocator = ConfigFileRelocator

View File

@ -186,6 +186,10 @@ getApp spec = case takeDecls @App (AS.decls spec) of
isAuthEnabled :: AppSpec -> Bool
isAuthEnabled spec = isJust (App.auth $ snd $ getApp spec)
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
getDbSystem :: AppSpec -> Maybe AS.Db.DbSystem
getDbSystem spec = AS.Db.system =<< AS.App.db (snd $ getApp spec)
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
isPostgresUsed :: AppSpec -> Bool
isPostgresUsed spec = Just AS.Db.PostgreSQL == (AS.Db.system =<< AS.App.db (snd $ getApp spec))
isPostgresUsed = (Just AS.Db.PostgreSQL ==) . getDbSystem

View File

@ -1,15 +0,0 @@
module Wasp.Common
( DbMigrationsDir,
WaspProjectDir,
dbMigrationsDirInWaspProjectDir,
)
where
import StrongPath (Dir, Path', Rel, reldir)
data WaspProjectDir -- Root dir of Wasp project, containing source files.
data DbMigrationsDir
dbMigrationsDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir DbMigrationsDir)
dbMigrationsDirInWaspProjectDir = [reldir|migrations|]

View File

@ -8,8 +8,8 @@ import qualified Data.Map as Data
import Data.Maybe (mapMaybe)
import StrongPath (Abs, Dir, File', Path', Rel)
import Wasp.AppSpec.ConfigFile (ConfigFileRelocator (..))
import Wasp.Common (WaspProjectDir)
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Project.Common (WaspProjectDir)
import qualified Wasp.Util.IO as Util.IO
type ConfigFileRelocationMap = Data.Map (Path' (Rel WaspProjectDir) File') (Path' (Rel ProjectRootDir) File')

View File

@ -0,0 +1,15 @@
module Wasp.Db.Postgres
( makeConnectionUrl,
postgresMaxDbNameLength,
)
where
import Text.Printf (printf)
makeConnectionUrl :: String -> String -> Int -> String -> String
makeConnectionUrl user pass port dbName =
printf "postgresql://%s:%s@localhost:%d/%s" user pass port dbName
-- As specified by PostgreSQL documentation.
postgresMaxDbNameLength :: Int
postgresMaxDbNameLength = 63

36
waspc/src/Wasp/Env.hs Normal file
View File

@ -0,0 +1,36 @@
-- | This modules implements general concepts regarding env vars.
-- It is not specific to Wasp in any way.
module Wasp.Env
( EnvVar,
EnvVarName,
EnvVarValue,
parseDotEnvFile,
envVarsToDotEnvContent,
)
where
import qualified Configuration.Dotenv as Dotenv
import Control.Exception (ErrorCall (ErrorCall))
import Data.List (intercalate)
import qualified Data.Text as T
import StrongPath (Abs, File, Path', fromAbsFile)
import UnliftIO.Exception (catch, throwIO)
type EnvVar = (EnvVarName, EnvVarValue)
type EnvVarName = String
type EnvVarValue = String
-- Reads specified dotenv file and returns its values.
-- Crashes if file doesn't exist or it can't parse it.
parseDotEnvFile :: Path' Abs (File ()) -> IO [EnvVar]
parseDotEnvFile envFile =
Dotenv.parseFile (fromAbsFile envFile)
-- Parse errors are returned from Dotenv.parseFile as ErrorCall, which Wasp compiler would
-- report as a bug in compiler, so we instead convert these to IOExceptions.
`catch` \(ErrorCall msg) -> throwIO $ userError $ "Failed to parse dot env file: " <> msg
envVarsToDotEnvContent :: [EnvVar] -> T.Text
envVarsToDotEnvContent vars =
T.pack $ intercalate "\n" $ map (\(name, value) -> name <> "=" <> show value) vars

View File

@ -1,6 +1,7 @@
module Wasp.Generator
( writeWebAppCode,
Wasp.Generator.Start.start,
ProjectRootDir,
)
where

View File

@ -7,10 +7,10 @@ where
import Data.Map (fromList)
import StrongPath (File', Path', Rel, castRel, relfile, (</>))
import Wasp.AppSpec (AppSpec, doesConfigFileExist)
import Wasp.Common (WaspProjectDir)
import Wasp.ConfigFile (ConfigFileRelocationMap)
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.WebAppGenerator.Common (webAppRootDirInProjectRootDir)
import Wasp.Project.Common (WaspProjectDir)
tailwindConfigFile :: Path' (Rel WaspProjectDir) File'
tailwindConfigFile = [relfile|tailwind.config.cjs|]

View File

@ -23,7 +23,6 @@ import Wasp.Generator.DbGenerator.Common
( DbSchemaChecksumFile,
DbSchemaChecksumOnLastDbConcurrenceFile,
PrismaDbSchema,
databaseUrlEnvVar,
dbMigrationsDirInDbRootDir,
dbRootDirInProjectRootDir,
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir,
@ -41,6 +40,7 @@ import Wasp.Generator.Monad
GeneratorWarning (GeneratorNeedsMigrationWarning),
logAndThrowGeneratorError,
)
import Wasp.Project.Db (databaseUrlEnvVarName)
import qualified Wasp.Psl.Ast.Model as Psl.Ast.Model
import qualified Wasp.Psl.Generator.Model as Psl.Generator.Model
import Wasp.Util (checksumFromFilePath, hexToString, ifM, (<:>))
@ -56,7 +56,7 @@ genPrismaSchema ::
Generator FileDraft
genPrismaSchema spec = do
(datasourceProvider :: String, datasourceUrl) <- case dbSystem of
AS.Db.PostgreSQL -> return ("postgresql", makeEnvVarField databaseUrlEnvVar)
AS.Db.PostgreSQL -> return ("postgresql", makeEnvVarField databaseUrlEnvVarName)
AS.Db.SQLite ->
if AS.isBuild spec
then logAndThrowGeneratorError $ GenericGeneratorError "SQLite (a default database) is not supported in production. To build your Wasp app for production, switch to a different database. Switching to PostgreSQL: https://wasp-lang.dev/docs/language/features#migrating-from-sqlite-to-postgresql ."
@ -67,12 +67,12 @@ genPrismaSchema spec = do
[ "modelSchemas" .= map entityToPslModelSchema (AS.getDecls @AS.Entity.Entity spec),
"datasourceProvider" .= datasourceProvider,
"datasourceUrl" .= datasourceUrl,
"prismaClientOutputDir" .= makeEnvVarField prismaClientOutputDirEnvVar
"prismaClientOutputDir" .= makeEnvVarField Wasp.Generator.DbGenerator.Common.prismaClientOutputDirEnvVar
]
return $ createTemplateFileDraft dbSchemaFileInProjectRootDir tmplSrcPath (Just templateData)
return $ createTemplateFileDraft Wasp.Generator.DbGenerator.Common.dbSchemaFileInProjectRootDir tmplSrcPath (Just templateData)
where
tmplSrcPath = dbTemplatesDirInTemplatesDir </> dbSchemaFileInDbTemplatesDir
tmplSrcPath = Wasp.Generator.DbGenerator.Common.dbTemplatesDirInTemplatesDir </> Wasp.Generator.DbGenerator.Common.dbSchemaFileInDbTemplatesDir
dbSystem = fromMaybe AS.Db.SQLite $ AS.Db.system =<< AS.App.db (snd $ getApp spec)
makeEnvVarField envVarName = "env(\"" ++ envVarName ++ "\")"
@ -87,7 +87,7 @@ genMigrationsDir spec =
AS.migrationsDir spec >>= \waspMigrationsDir ->
Just $ createCopyDirFileDraft (SP.castDir genProjectMigrationsDir) (SP.castDir waspMigrationsDir)
where
genProjectMigrationsDir = dbRootDirInProjectRootDir </> dbMigrationsDirInDbRootDir
genProjectMigrationsDir = Wasp.Generator.DbGenerator.Common.dbRootDirInProjectRootDir </> Wasp.Generator.DbGenerator.Common.dbMigrationsDirInDbRootDir
-- | This function operates on generated code, and thus assumes the file drafts were written to disk
postWriteDbGeneratorActions :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ([GeneratorWarning], [GeneratorError])
@ -125,19 +125,22 @@ warnIfDbNeedsMigration spec projectRootDir = do
then warnProjectDiffersFromDb projectRootDir
else return Nothing
where
dbSchemaFp = projectRootDir </> dbSchemaFileInProjectRootDir
dbSchemaChecksumFp = projectRootDir </> dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
dbSchemaFp = projectRootDir </> Wasp.Generator.DbGenerator.Common.dbSchemaFileInProjectRootDir
dbSchemaChecksumFp = projectRootDir </> Wasp.Generator.DbGenerator.Common.dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
entitiesExist = not . null $ getEntities spec
warnIfSchemaDiffersFromChecksum ::
Path' Abs (File PrismaDbSchema) ->
Path' Abs (File DbSchemaChecksumOnLastDbConcurrenceFile) ->
Path' Abs (File Wasp.Generator.DbGenerator.Common.PrismaDbSchema) ->
Path' Abs (File Wasp.Generator.DbGenerator.Common.DbSchemaChecksumOnLastDbConcurrenceFile) ->
IO (Maybe GeneratorWarning)
warnIfSchemaDiffersFromChecksum dbSchemaFileAbs dbschemachecksumfile =
ifM
(checksumFileMatchesSchema dbSchemaFileAbs dbschemachecksumfile)
(return Nothing)
(return . Just $ GeneratorNeedsMigrationWarning "Your Prisma schema has changed, please run `wasp db migrate-dev` when ready.")
( return . Just $
GeneratorNeedsMigrationWarning
"Your Prisma schema has changed, please run `wasp db migrate-dev` when ready."
)
-- | Checks if the project's Prisma schema file and migrations dir matches the DB state.
-- Issues a warning if it cannot connect, or if either check fails.
@ -151,14 +154,22 @@ warnProjectDiffersFromDb projectRootDir = do
then do
-- NOTE: Since we know schema == db and all migrations are applied,
-- we can write this file to prevent future redundant Prisma checks.
DbOps.writeDbSchemaChecksumToFile projectRootDir dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
DbOps.writeDbSchemaChecksumToFile projectRootDir Wasp.Generator.DbGenerator.Common.dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
return Nothing
else return . Just $ GeneratorNeedsMigrationWarning "You have unapplied migrations. Please run `wasp db migrate-dev` when ready."
Just False -> return . Just $ GeneratorNeedsMigrationWarning "Your Prisma schema does not match your database, please run `wasp db migrate-dev`."
-- NOTE: If there was an error, it could mean we could not connect to the SQLite db, since it does not exist.
-- Or it could mean their databaseUrlEnvVar is wrong, or database is down, or any other number of causes.
-- In any case, migrating will either solve it (in the SQLite case), or allow Prisma to give them enough info to troubleshoot.
Nothing -> return . Just $ GeneratorNeedsMigrationWarning "Wasp was unable to verify your database is up to date. Running `wasp db migrate-dev` may fix this and will provide more info."
else
return . Just . GeneratorNeedsMigrationWarning $
"You have unapplied migrations. Please run `wasp db migrate-dev` when ready."
Just False ->
return . Just . GeneratorNeedsMigrationWarning $
"Your Prisma schema does not match your database, please run `wasp db migrate-dev`."
-- NOTE: If there was an error, it could mean we could not connect to the SQLite db, since it
-- does not exist. Or it could mean their databaseUrlEnvVar is wrong, or database is down, or
-- any other number of causes. In any case, migrating will either solve it (in the SQLite case),
-- or allow Prisma to give them enough info to troubleshoot.
Nothing ->
return . Just . GeneratorNeedsMigrationWarning $
"Wasp was unable to verify your database is up to date."
<> " Running `wasp db migrate-dev` may fix this and will provide more info."
genPrismaClients :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorError)
genPrismaClients spec projectRootDir =
@ -169,7 +180,9 @@ genPrismaClients spec projectRootDir =
where
wasCurrentSchemaAlreadyGenerated :: IO Bool
wasCurrentSchemaAlreadyGenerated =
checksumFileExistsAndMatchesSchema projectRootDir dbSchemaChecksumOnLastGenerateFileProjectRootDir
checksumFileExistsAndMatchesSchema
projectRootDir
Wasp.Generator.DbGenerator.Common.dbSchemaChecksumOnLastGenerateFileProjectRootDir
generatePrismaClientsIfEntitiesExist :: IO (Maybe GeneratorError)
generatePrismaClientsIfEntitiesExist
@ -180,7 +193,7 @@ genPrismaClients spec projectRootDir =
entitiesExist = not . null $ getEntities spec
checksumFileExistsAndMatchesSchema ::
DbSchemaChecksumFile f =>
Wasp.Generator.DbGenerator.Common.DbSchemaChecksumFile f =>
Path' Abs (Dir ProjectRootDir) ->
Path' (Rel ProjectRootDir) (File f) ->
IO Bool
@ -190,16 +203,16 @@ checksumFileExistsAndMatchesSchema projectRootDir dbSchemaChecksumInProjectDir =
(checksumFileMatchesSchema dbSchemaFileAbs checksumFileAbs)
(return False)
where
dbSchemaFileAbs = projectRootDir </> dbSchemaFileInProjectRootDir
dbSchemaFileAbs = projectRootDir </> Wasp.Generator.DbGenerator.Common.dbSchemaFileInProjectRootDir
checksumFileAbs = projectRootDir </> dbSchemaChecksumInProjectDir
checksumFileMatchesSchema :: DbSchemaChecksumFile f => Path' Abs (File PrismaDbSchema) -> Path' Abs (File f) -> IO Bool
checksumFileMatchesSchema :: Wasp.Generator.DbGenerator.Common.DbSchemaChecksumFile f => Path' Abs (File Wasp.Generator.DbGenerator.Common.PrismaDbSchema) -> Path' Abs (File f) -> IO Bool
checksumFileMatchesSchema dbSchemaFileAbs dbSchemaChecksumFileAbs = do
-- Read file strictly as the checksum may be later overwritten.
dbChecksumFileContents <- IOUtil.readFileStrict dbSchemaChecksumFileAbs
schemaFileHasChecksum dbSchemaFileAbs dbChecksumFileContents
where
schemaFileHasChecksum :: Path' Abs (File PrismaDbSchema) -> Text -> IO Bool
schemaFileHasChecksum :: Path' Abs (File Wasp.Generator.DbGenerator.Common.PrismaDbSchema) -> Text -> IO Bool
schemaFileHasChecksum schemaFile checksum = do
dbSchemaFileChecksum <- pack . hexToString <$> checksumFromFilePath schemaFile
return $ dbSchemaFileChecksum == checksum

View File

@ -20,16 +20,15 @@ module Wasp.Generator.DbGenerator.Common
webAppRootDirFromDbRootDir,
dbSchemaFileInProjectRootDir,
prismaClientOutputDirEnvVar,
databaseUrlEnvVar,
DbSchemaChecksumFile,
)
where
import StrongPath (Dir, File, File', Path', Rel, reldir, relfile, (</>))
import qualified StrongPath as SP
import Wasp.Common (DbMigrationsDir)
import Wasp.Generator.Common (AppComponentRootDir, DbRootDir, ProjectRootDir, ServerRootDir)
import Wasp.Generator.Templates (TemplatesDir)
import Wasp.Project.Db.Migrations (DbMigrationsDir)
data DbTemplatesDir
@ -97,9 +96,6 @@ dbSchemaChecksumOnLastGenerateFileProjectRootDir = dbRootDirInProjectRootDir </>
prismaClientOutputDirEnvVar :: String
prismaClientOutputDirEnvVar = "PRISMA_CLIENT_OUTPUT_DIR"
databaseUrlEnvVar :: String
databaseUrlEnvVar = "DATABASE_URL"
prismaClientOutputDirInAppComponentDir :: AppComponentRootDir d => Path' (Rel d) (Dir ServerRootDir)
prismaClientOutputDirInAppComponentDir = [reldir|node_modules/.prisma/client|]

View File

@ -3,18 +3,20 @@ module Wasp.Generator.DbGenerator.Jobs
migrateDiff,
generatePrismaClient,
runStudio,
reset,
migrateStatus,
asPrismaCliArgs,
)
where
import StrongPath (Abs, Dir, File', Path', (</>))
import StrongPath (Abs, Dir, File, File', Path', (</>))
import qualified StrongPath as SP
import StrongPath.TH (relfile)
import qualified System.Info
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.DbGenerator.Common
( MigrateArgs (..),
PrismaDbSchema,
dbSchemaFileInProjectRootDir,
)
import Wasp.Generator.Job (JobType)
@ -23,10 +25,7 @@ import Wasp.Generator.Job.Process (runNodeCommandAsJob, runNodeCommandAsJobWithE
import Wasp.Generator.ServerGenerator.Common (serverRootDirInProjectRootDir)
migrateDev :: Path' Abs (Dir ProjectRootDir) -> MigrateArgs -> J.Job
migrateDev projectDir migrateArgs = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir
migrateDev projectDir migrateArgs =
-- NOTE(matija): We are running this command from server's root dir since that is where
-- Prisma packages (cli and client) are currently installed.
-- NOTE(martin): `prisma migrate dev` refuses to execute when interactivity is needed if stdout is being piped,
@ -34,25 +33,29 @@ migrateDev projectDir migrateArgs = do
-- so we do have interactivity, but Prisma doesn't know that.
-- I opened an issue with Prisma https://github.com/prisma/prisma/issues/7113, but in the meantime
-- we are using `script` to trick Prisma into thinking it is running in TTY (interactively).
-- NOTE(martin): For this to work on Mac, filepath in the list below must be as it is now - not wrapped in any quotes.
let prismaMigrateCmd =
[ absPrismaExecutableFp projectDir,
"migrate",
"dev",
"--schema",
SP.toFilePath schemaFile,
"--skip-generate"
]
++ asPrismaCliArgs migrateArgs
let scriptArgs =
if System.Info.os == "darwin"
then -- NOTE(martin): On MacOS, command that `script` should execute is treated as multiple arguments.
["-Fq", "/dev/null"] ++ prismaMigrateCmd
else -- NOTE(martin): On Linux, command that `script` should execute is treated as one argument.
["-feqc", unwords prismaMigrateCmd, "/dev/null"]
runNodeCommandAsJob serverDir "script" scriptArgs J.Db
where
serverDir = projectDir </> serverRootDirInProjectRootDir
schemaFile = projectDir </> dbSchemaFileInProjectRootDir
scriptArgs =
if System.Info.os == "darwin"
then -- NOTE(martin): On MacOS, command that `script` should execute is treated as multiple arguments.
["-Fq", "/dev/null"] ++ prismaMigrateCmd
else -- NOTE(martin): On Linux, command that `script` should execute is treated as one argument.
["-feqc", unwords prismaMigrateCmd, "/dev/null"]
-- NOTE(martin): For this to work on Mac, filepath in the list below must be as it is now - not
-- wrapped in any quotes.
prismaMigrateCmd =
[ absPrismaExecutableFp projectDir,
"migrate",
"dev",
"--schema",
SP.fromAbsFile schemaFile,
"--skip-generate"
]
++ asPrismaCliArgs migrateArgs
asPrismaCliArgs :: MigrateArgs -> [String]
asPrismaCliArgs migrateArgs = do
@ -65,20 +68,15 @@ asPrismaCliArgs migrateArgs = do
-- Because of the --exit-code flag, it changes the exit code behavior
-- to signal if the diff is empty or not (Empty: 0, Error: 1, Not empty: 2)
migrateDiff :: Path' Abs (Dir ProjectRootDir) -> J.Job
migrateDiff projectDir = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
let schemaFileFp = SP.toFilePath $ projectDir </> dbSchemaFileInProjectRootDir
let prismaMigrateDiffCmdArgs =
[ "migrate",
"diff",
"--from-schema-datamodel",
schemaFileFp,
"--to-schema-datasource",
schemaFileFp,
"--exit-code"
]
runNodeCommandAsJob serverDir (absPrismaExecutableFp projectDir) prismaMigrateDiffCmdArgs J.Db
migrateDiff projectDir = runPrismaCommandAsJob projectDir $ \schema ->
[ "migrate",
"diff",
"--from-schema-datamodel",
SP.fromAbsFile schema,
"--to-schema-datasource",
SP.fromAbsFile schema,
"--exit-code"
]
-- | Checks to see if all migrations are applied to the DB.
-- An exit code of 0 means we successfully verified all migrations are applied.
@ -86,26 +84,19 @@ migrateDiff projectDir = do
-- or (b) there are pending migrations to apply.
-- Therefore, this should be checked **after** a command that ensures connectivity.
migrateStatus :: Path' Abs (Dir ProjectRootDir) -> J.Job
migrateStatus projectDir = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
let schemaFileFp = SP.toFilePath $ projectDir </> dbSchemaFileInProjectRootDir
let prismaMigrateDiffCmdArgs =
[ "migrate",
"status",
"--schema",
schemaFileFp
]
migrateStatus projectDir = runPrismaCommandAsJob projectDir $ \schema ->
["migrate", "status", "--schema", SP.fromAbsFile schema]
runNodeCommandAsJob serverDir (absPrismaExecutableFp projectDir) prismaMigrateDiffCmdArgs J.Db
-- | Runs `prisma migrate reset`, which drops the tables (so schemas and data is lost) and then
-- reapplies all the migrations.
reset :: Path' Abs (Dir ProjectRootDir) -> J.Job
reset projectDir = runPrismaCommandAsJob projectDir $ \schema ->
["migrate", "reset", "--skip-generate", "--schema", SP.fromAbsFile schema]
-- | Runs `prisma studio` - Prisma's db inspector.
runStudio :: Path' Abs (Dir ProjectRootDir) -> J.Job
runStudio projectDir =
runNodeCommandAsJob serverDir (absPrismaExecutableFp projectDir) prismaStudioCmdArgs J.Db
where
serverDir = projectDir </> serverRootDirInProjectRootDir
schemaFile = projectDir </> dbSchemaFileInProjectRootDir
prismaStudioCmdArgs = ["studio", "--schema", SP.toFilePath schemaFile]
runStudio projectDir = runPrismaCommandAsJob projectDir $ \schema ->
["studio", "--schema", SP.fromAbsFile schema]
generatePrismaClient :: Path' Abs (Dir ProjectRootDir) -> (String, String) -> JobType -> J.Job
generatePrismaClient projectDir prismaClientOutputDirEnv jobType =
@ -114,8 +105,18 @@ generatePrismaClient projectDir prismaClientOutputDirEnv jobType =
envVars = [prismaClientOutputDirEnv]
serverRootDir = projectDir </> serverRootDirInProjectRootDir
prismaExecutable = absPrismaExecutableFp projectDir
prismaGenerateCmdArgs = ["generate", "--schema", schemaFile]
schemaFile = SP.fromAbsFile $ projectDir </> dbSchemaFileInProjectRootDir
prismaGenerateCmdArgs = ["generate", "--schema", SP.fromAbsFile schemaFile]
schemaFile = projectDir </> dbSchemaFileInProjectRootDir
runPrismaCommandAsJob ::
Path' Abs (Dir ProjectRootDir) ->
(Path' Abs (File PrismaDbSchema) -> [String]) ->
J.Job
runPrismaCommandAsJob projectDir makeCmdArgs =
runNodeCommandAsJob serverDir (absPrismaExecutableFp projectDir) (makeCmdArgs schemaFile) J.Db
where
serverDir = projectDir </> serverRootDirInProjectRootDir
schemaFile = projectDir </> dbSchemaFileInProjectRootDir
-- | NOTE: The expectation is that `npm install` was already executed
-- such that we can use the locally installed package.

View File

@ -4,6 +4,7 @@ module Wasp.Generator.DbGenerator.Operations
doesSchemaMatchDb,
writeDbSchemaChecksumToFile,
areAllMigrationsAppliedToDb,
dbReset,
)
where
@ -18,7 +19,6 @@ import qualified Path as P
import StrongPath (Abs, Dir, File, Path', Rel, (</>))
import qualified StrongPath as SP
import System.Exit (ExitCode (..))
import Wasp.Common (DbMigrationsDir)
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.DbGenerator.Common
( DbSchemaChecksumFile,
@ -38,8 +38,9 @@ import Wasp.Generator.FileDraft.WriteableMonad (WriteableMonad (copyDirectoryRec
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.IO (printJobMsgsUntilExitReceived, readJobMessagesAndPrintThemPrefixed)
import qualified Wasp.Generator.WriteFileDrafts as Generator.WriteFileDrafts
import Wasp.Project.Db.Migrations (DbMigrationsDir)
import Wasp.Util (checksumFromFilePath, hexToString)
import Wasp.Util.IO (doesFileExist, removeFile)
import Wasp.Util.IO (deleteFileIfExists, doesFileExist)
import qualified Wasp.Util.IO as IOUtil
-- | Migrates in the generated project context and then copies the migrations dir back
@ -101,10 +102,25 @@ removeDbSchemaChecksumFile ::
Path' Abs (Dir ProjectRootDir) ->
Path' (Rel ProjectRootDir) (File f) ->
IO ()
removeDbSchemaChecksumFile genProjectRootDirAbs dbSchemaChecksumInProjectRootDir = removeFile dbSchemaChecksumFp
removeDbSchemaChecksumFile genProjectRootDirAbs dbSchemaChecksumInProjectRootDir = deleteFileIfExists dbSchemaChecksumFp
where
dbSchemaChecksumFp = genProjectRootDirAbs </> dbSchemaChecksumInProjectRootDir
-- Resets the database: drops all data and applies all migrations from scratch.
dbReset ::
Path' Abs (Dir ProjectRootDir) ->
IO (Either String ())
dbReset genProjectDir = do
-- We are doing quite a move here, resetting the whole db, so best to delete the checksum file,
-- which will force Wasp to do a deep check of migrations next time, just to be sure.
removeDbSchemaChecksumFile genProjectDir dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
chan <- newChan
((), exitCode) <-
readJobMessagesAndPrintThemPrefixed chan `concurrently` DbJobs.reset genProjectDir chan
return $ case exitCode of
ExitSuccess -> Right ()
ExitFailure c -> Left $ "Failed with exit code " <> show c
generatePrismaClients :: Path' Abs (Dir ProjectRootDir) -> IO (Either String ())
generatePrismaClients projectRootDir = do
generateResult <- liftA2 (>>) generatePrismaClientForServer generatePrismaClientForWebApp projectRootDir

View File

@ -6,6 +6,7 @@ module Wasp.Generator.FileDraft
createCopyFileDraftIfExists,
createTextFileDraft,
createCopyDirFileDraft,
createCopyAndModifyTextFileDraft,
)
where
@ -14,6 +15,7 @@ import Data.Text (Text)
import StrongPath (Abs, Dir, File, Path', Rel)
import qualified StrongPath as SP
import Wasp.Generator.Common (ProjectRootDir)
import qualified Wasp.Generator.FileDraft.CopyAndModifyTextFileDraft as CMTextFD
import qualified Wasp.Generator.FileDraft.CopyDirFileDraft as CopyDirFD
import qualified Wasp.Generator.FileDraft.CopyFileDraft as CopyFD
import qualified Wasp.Generator.FileDraft.TemplateFileDraft as TmplFD
@ -33,23 +35,26 @@ data FileDraft
| FileDraftCopyFd CopyFD.CopyFileDraft
| FileDraftCopyDirFd CopyDirFD.CopyDirFileDraft
| FileDraftTextFd TextFD.TextFileDraft
deriving (Show, Eq)
| FileDraftCopyAndModifyTextFd CMTextFD.CopyAndModifyTextFileDraft
instance Writeable FileDraft where
write dstDir (FileDraftTemplateFd draft) = write dstDir draft
write dstDir (FileDraftCopyFd draft) = write dstDir draft
write dstDir (FileDraftCopyDirFd draft) = write dstDir draft
write dstDir (FileDraftTextFd draft) = write dstDir draft
write dstDir (FileDraftCopyAndModifyTextFd draft) = write dstDir draft
getChecksum (FileDraftTemplateFd draft) = getChecksum draft
getChecksum (FileDraftCopyFd draft) = getChecksum draft
getChecksum (FileDraftCopyDirFd draft) = getChecksum draft
getChecksum (FileDraftTextFd draft) = getChecksum draft
getChecksum (FileDraftCopyAndModifyTextFd draft) = getChecksum draft
getDstPath (FileDraftTemplateFd draft) = getDstPath draft
getDstPath (FileDraftCopyFd draft) = getDstPath draft
getDstPath (FileDraftCopyDirFd draft) = getDstPath draft
getDstPath (FileDraftTextFd draft) = getDstPath draft
getDstPath (FileDraftCopyAndModifyTextFd draft) = getDstPath draft
createTemplateFileDraft ::
Path' (Rel ProjectRootDir) (File a) ->
@ -73,6 +78,17 @@ createCopyFileDraft dstPath srcPath =
CopyFD._failIfSrcDoesNotExist = True
}
createCopyAndModifyTextFileDraft ::
Path' (Rel ProjectRootDir) (File a) -> Path' Abs (File b) -> (Text -> Text) -> FileDraft
createCopyAndModifyTextFileDraft dstPath srcPath modify =
FileDraftCopyAndModifyTextFd $
CMTextFD.CopyAndModifyTextFileDraft
{ CMTextFD._dstPath = SP.castFile dstPath,
CMTextFD._srcPath = SP.castFile srcPath,
CMTextFD._modify = modify,
CMTextFD._failIfSrcDoesNotExist = True
}
createCopyFileDraftIfExists :: Path' (Rel ProjectRootDir) (File a) -> Path' Abs (File b) -> FileDraft
createCopyFileDraftIfExists dstPath srcPath =
FileDraftCopyFd $

View File

@ -0,0 +1,54 @@
module Wasp.Generator.FileDraft.CopyAndModifyTextFileDraft
( CopyAndModifyTextFileDraft (..),
)
where
import Control.Monad (when)
import qualified Data.Text as T
import qualified Data.Text.IO as T.IO
import StrongPath (Abs, File', Path', Rel, (</>))
import qualified StrongPath as SP
import System.IO.Error (doesNotExistErrorType, mkIOError)
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.FileDraft.Writeable
import Wasp.Generator.FileDraft.WriteableMonad
import Wasp.Util (checksumFromText)
-- | File draft based on existing text file + some modifications.
data CopyAndModifyTextFileDraft = CopyAndModifyTextFileDraft
{ -- | Path where the file will be copied to.
_dstPath :: !(Path' (Rel ProjectRootDir) File'),
-- | Absolute path of source file to copy.
_srcPath :: !(Path' Abs File'),
_failIfSrcDoesNotExist :: !Bool,
_modify :: !(T.Text -> T.Text)
}
instance Writeable CopyAndModifyTextFileDraft where
write absDstDirPath draft = do
srcFileExists <- doesFileExist (SP.fromAbsFile $ _srcPath draft)
if srcFileExists
then do
createDirectoryIfMissing True (SP.fromAbsDir $ SP.parent absDraftDstPath)
readFileAsText (SP.fromAbsFile $ _srcPath draft)
>>= writeFileFromText (SP.fromAbsFile absDraftDstPath) . _modify draft
else
when
(_failIfSrcDoesNotExist draft)
( throwIO $
mkIOError
doesNotExistErrorType
"Source file of CopyTextFileDraft does not exist."
Nothing
(Just $ SP.fromAbsFile $ _srcPath draft)
)
where
absDraftDstPath = absDstDirPath </> _dstPath draft
-- NOTE: We are reading file here, but then also again later in the "write", so
-- we are reading file at least twice! Any way to avoid it?
-- Idea: We could cache it in an mvar in the CopyTextFileDraft.
getChecksum draft =
checksumFromText . _modify draft <$> T.IO.readFile (SP.fromAbsFile $ _srcPath draft)
getDstPath draft = Left $ _dstPath draft

View File

@ -19,7 +19,7 @@ data CopyFileDraft = CopyFileDraft
_dstPath :: !(Path' (Rel ProjectRootDir) File'),
-- | Absolute path of source file to copy.
_srcPath :: !(Path' Abs File'),
_failIfSrcDoesNotExist :: Bool
_failIfSrcDoesNotExist :: !Bool
}
deriving (Show, Eq)

View File

@ -54,6 +54,8 @@ class (MonadIO m) => WriteableMonad m where
writeFileFromText :: FilePath -> Text -> m ()
readFileAsText :: FilePath -> m Text
getTemplateFileAbsPath ::
-- | Template file path.
Path' (Rel Templates.TemplatesDir) (File a) ->
@ -94,6 +96,7 @@ instance WriteableMonad IO where
doesFileExist = System.Directory.doesFileExist
doesDirectoryExist = System.Directory.doesDirectoryExist
writeFileFromText = Data.Text.IO.writeFile
readFileAsText = Data.Text.IO.readFile
getTemplateFileAbsPath = Templates.getTemplateFileAbsPath
getTemplatesDirAbsPath = Templates.getTemplatesDirAbsPath
compileAndRenderTemplate = Templates.compileAndRenderTemplate

View File

@ -1,4 +1,7 @@
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Replace case with maybe" #-}
module Wasp.Generator.ServerGenerator
( genServer,
@ -36,6 +39,7 @@ import qualified Wasp.AppSpec.App.Server as AS.App.Server
import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.AppSpec.Util (isPgBossJobExecutorUsed)
import Wasp.AppSpec.Valid (getApp, isAuthEnabled)
import Wasp.Env (envVarsToDotEnvContent)
import Wasp.Generator.Common
( ServerRootDir,
latestMajorNodeVersion,
@ -44,7 +48,7 @@ import Wasp.Generator.Common
prismaVersion,
)
import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir)
import Wasp.Generator.FileDraft (FileDraft, createCopyFileDraft)
import Wasp.Generator.FileDraft (FileDraft, createTextFileDraft)
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.NpmDependencies as N
import Wasp.Generator.ServerGenerator.ApiRoutesG (genApis)
@ -57,6 +61,7 @@ import Wasp.Generator.ServerGenerator.JobGenerator (depsRequiredByJobs, genJobEx
import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson)
import Wasp.Generator.ServerGenerator.OperationsG (genOperations)
import Wasp.Generator.ServerGenerator.OperationsRoutesG (genOperationsRoutes)
import Wasp.Project.Db (databaseUrlEnvVarName)
import Wasp.SemanticVersion (major)
import Wasp.Util (toLowerFirst, (<++>))
@ -85,15 +90,22 @@ genServer spec =
genFileCopy = return . C.mkTmplFd
genDotEnv :: AppSpec -> Generator [FileDraft]
genDotEnv spec = return $
case AS.dotEnvServerFile spec of
Just srcFilePath
| not $ AS.isBuild spec ->
[ createCopyFileDraft
(C.serverRootDirInProjectRootDir </> dotEnvInServerRootDir)
srcFilePath
]
_ -> []
-- Don't generate .env if we are building for production, since .env is to be used only for
-- development.
genDotEnv spec | AS.isBuild spec = return []
genDotEnv spec =
return
[ createTextFileDraft
(C.serverRootDirInProjectRootDir </> dotEnvInServerRootDir)
(envVarsToDotEnvContent envVars)
]
where
envVars = waspEnvVars ++ userEnvVars
userEnvVars = AS.devEnvVarsServer spec
waspEnvVars = case AS.devDatabaseUrl spec of
Just url | not isThereCustomDbUrl -> [(databaseUrlEnvVarName, url)]
_ -> []
isThereCustomDbUrl = any ((== databaseUrlEnvVarName) . fst) userEnvVars
dotEnvInServerRootDir :: Path' (Rel ServerRootDir) File'
dotEnvInServerRootDir = [relfile|.env|]

View File

@ -5,7 +5,6 @@ module Wasp.Generator.ServerGenerator.Common
mkTmplFd,
mkTmplFdWithDstAndData,
mkSrcTmplFd,
dotEnvServer,
srcDirInServerTemplatesDir,
asTmplFile,
asTmplSrcFile,
@ -22,11 +21,16 @@ module Wasp.Generator.ServerGenerator.Common
where
import qualified Data.Aeson as Aeson
import StrongPath (Dir, File', Path', Rel, reldir, relfile, (</>))
import StrongPath (Dir, File', Path', Rel, reldir, (</>))
import qualified StrongPath as SP
import System.FilePath (splitExtension)
import Wasp.Common (WaspProjectDir)
import Wasp.Generator.Common (GeneratedSrcDir, ProjectRootDir, ServerRootDir, UniversalTemplatesDir, universalTemplatesDirInTemplatesDir)
import Wasp.Generator.Common
( GeneratedSrcDir,
ProjectRootDir,
ServerRootDir,
UniversalTemplatesDir,
universalTemplatesDirInTemplatesDir,
)
import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft)
import Wasp.Generator.Templates (TemplatesDir)
@ -110,9 +114,6 @@ serverTemplatesDirInTemplatesDir = [reldir|server|]
srcDirInServerTemplatesDir :: Path' (Rel ServerTemplatesDir) (Dir ServerTemplatesSrcDir)
srcDirInServerTemplatesDir = [reldir|src|]
dotEnvServer :: Path' (SP.Rel WaspProjectDir) File'
dotEnvServer = [relfile|.env.server|]
-- Converts the real name of the source file (i.e., name on disk) into a name
-- that can be used in an ESNext import.
-- Specifically, when using the ESNext module system, all source files must be

View File

@ -9,10 +9,10 @@ import StrongPath (File', Path', Rel, relfile, (</>))
import qualified StrongPath as SP
import Wasp.AppSpec (AppSpec)
import Wasp.AppSpec.Valid (isAuthEnabled)
import Wasp.Generator.DbGenerator.Common (databaseUrlEnvVar)
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.ServerGenerator.Common as C
import Wasp.Project.Db (databaseUrlEnvVarName)
genConfigFile :: AppSpec -> Generator FileDraft
genConfigFile spec = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
@ -22,7 +22,7 @@ genConfigFile spec = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tm
tmplData =
object
[ "isAuthEnabled" .= isAuthEnabled spec,
"databaseUrlEnvVar" .= databaseUrlEnvVar
"databaseUrlEnvVarName" .= databaseUrlEnvVarName
]
configFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'

View File

@ -24,10 +24,11 @@ import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
import Wasp.AppSpec.App.Client as AS.App.Client
import qualified Wasp.AppSpec.App.Client as AS.App.Client
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.AppSpec.Valid (getApp)
import Wasp.Env (envVarsToDotEnvContent)
import Wasp.Generator.AuthProviders (gitHubAuthProvider, googleAuthProvider)
import qualified Wasp.Generator.AuthProviders.OAuth as OAuth
import Wasp.Generator.Common
@ -75,15 +76,15 @@ genWebApp spec = do
genFileCopy = return . C.mkTmplFd
genDotEnv :: AppSpec -> Generator [FileDraft]
genDotEnv spec = return $
case AS.dotEnvClientFile spec of
Just srcFilePath
| not $ AS.isBuild spec ->
[ createCopyFileDraft
(C.webAppRootDirInProjectRootDir </> dotEnvInWebAppRootDir)
srcFilePath
]
_ -> []
-- Don't generate .env if we are building for production, since .env is to be used only for
-- development.
genDotEnv spec | AS.isBuild spec = return []
genDotEnv spec =
return
[ createTextFileDraft
(C.webAppRootDirInProjectRootDir </> dotEnvInWebAppRootDir)
(envVarsToDotEnvContent $ AS.devEnvVarsClient spec)
]
dotEnvInWebAppRootDir :: Path' (Rel C.WebAppRootDir) File'
dotEnvInWebAppRootDir = [relfile|.env|]

View File

@ -1,7 +1,6 @@
module Wasp.Generator.WebAppGenerator.Common
( webAppRootDirInProjectRootDir,
webAppSrcDirInWebAppRootDir,
dotEnvClient,
mkSrcTmplFd,
mkTmplFd,
mkTmplFdWithDst,
@ -21,9 +20,8 @@ module Wasp.Generator.WebAppGenerator.Common
where
import qualified Data.Aeson as Aeson
import StrongPath (Dir, File', Path', Rel, reldir, relfile, (</>))
import StrongPath (Dir, File', Path', Rel, reldir, (</>))
import qualified StrongPath as SP
import Wasp.Common (WaspProjectDir)
import Wasp.Generator.Common (GeneratedSrcDir, ProjectRootDir, UniversalTemplatesDir, WebAppRootDir, universalTemplatesDirInTemplatesDir)
import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft)
import Wasp.Generator.Templates (TemplatesDir)
@ -65,9 +63,6 @@ webAppTemplatesDirInTemplatesDir = [reldir|react-app|]
srcDirInWebAppTemplatesDir :: Path' (Rel WebAppTemplatesDir) (Dir WebAppTemplatesSrcDir)
srcDirInWebAppTemplatesDir = [reldir|src|]
dotEnvClient :: Path' (SP.Rel WaspProjectDir) File'
dotEnvClient = [relfile|.env.client|]
mkSrcTmplFd :: Path' (Rel WebAppTemplatesSrcDir) File' -> FileDraft
mkSrcTmplFd pathInTemplatesSrcDir = mkTmplFdWithDst srcPath dstPath
where

View File

@ -1,201 +0,0 @@
module Wasp.Lib
( compile,
Generator.start,
ProjectRootDir,
findWaspFile,
analyzeWaspProject,
compileAndRenderDockerfile,
CompileError,
CompileWarning,
deploy,
)
where
import Control.Arrow
import Control.Concurrent (newChan)
import Control.Concurrent.Async (concurrently)
import Control.Monad.Except
import Control.Monad.Extra (whenMaybeM)
import Data.List (find, isSuffixOf)
import Data.List.NonEmpty (toList)
import Data.Maybe (maybeToList)
import Data.Text (Text)
import qualified Data.Text.IO as T.IO
import StrongPath (Abs, Dir, File', Path', Rel, fromAbsDir, reldir, relfile, toFilePath, (</>))
import System.Directory (doesDirectoryExist, doesFileExist)
import qualified Wasp.Analyzer as Analyzer
import Wasp.Analyzer.AnalyzeError (getErrorMessageAndCtx)
import qualified Wasp.AppSpec as AS
import Wasp.AppSpec.Valid (validateAppSpec)
import Wasp.Common (DbMigrationsDir, WaspProjectDir, dbMigrationsDirInWaspProjectDir)
import Wasp.CompileOptions (CompileOptions (generatorWarningsFilter), sendMessage)
import qualified Wasp.CompileOptions as CompileOptions
import qualified Wasp.ConfigFile as CF
import qualified Wasp.Data as Data
import Wasp.Error (showCompilerErrorForTerminal)
import qualified Wasp.ExternalCode as ExternalCode
import qualified Wasp.Generator as Generator
import Wasp.Generator.Common (ProjectRootDir)
import qualified Wasp.Generator.ConfigFile as G.CF
import qualified Wasp.Generator.DockerGenerator as DockerGenerator
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.IO (printJobMsgsUntilExitReceived)
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
import Wasp.Generator.ServerGenerator.Common (dotEnvServer)
import Wasp.Generator.WebAppGenerator.Common (dotEnvClient)
import Wasp.Util (maybeToEither, unlessM)
import qualified Wasp.Util.IO as IOUtil
type CompileError = String
type CompileWarning = String
compile ::
Path' Abs (Dir WaspProjectDir) ->
Path' Abs (Dir ProjectRootDir) ->
CompileOptions ->
IO ([CompileWarning], [CompileError])
compile waspDir outDir options = do
compileWarningsAndErrors <-
analyzeWaspProject waspDir options >>= \case
Left analyzerErrors -> return ([], analyzerErrors)
Right appSpec -> generateCode appSpec outDir options
dotEnvWarnings <- maybeToList <$> warnIfDotEnvPresent waspDir
return $ (dotEnvWarnings, []) <> compileWarningsAndErrors
analyzeWaspProject ::
Path' Abs (Dir WaspProjectDir) ->
CompileOptions ->
IO (Either [CompileError] AS.AppSpec)
analyzeWaspProject waspDir options = runExceptT $ do
waspFilePath <- ExceptT $ left pure <$> findWaspFile waspDir
declarations <- ExceptT $ left pure <$> analyzeWaspFileContent waspFilePath
ExceptT $ constructAppSpec waspDir options declarations
generateCode ::
AS.AppSpec ->
Path' Abs (Dir ProjectRootDir) ->
CompileOptions ->
IO ([CompileError], [CompileWarning])
generateCode appSpec outDir options = do
(generatorWarnings, generatorErrors) <- Generator.writeWebAppCode appSpec outDir (sendMessage options)
let filteredWarnings = generatorWarningsFilter options generatorWarnings
return (show <$> filteredWarnings, show <$> generatorErrors)
-- | Checks the wasp directory for potential problems, and issues warnings if any are found.
warnIfDotEnvPresent :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe CompileWarning)
warnIfDotEnvPresent waspDir = (warningMessage <$) <$> findDotEnv waspDir
where
warningMessage = "Wasp .env files should be named .env.server or .env.client, depending on their use."
analyzeWaspFileContent :: Path' Abs File' -> IO (Either CompileError [AS.Decl])
analyzeWaspFileContent waspFilePath = do
waspFileContent <- IOUtil.readFile waspFilePath
let declsOrAnalyzeError = Analyzer.analyze waspFileContent
return $
left
(showCompilerErrorForTerminal (waspFilePath, waspFileContent) . getErrorMessageAndCtx)
declsOrAnalyzeError
constructAppSpec ::
Path' Abs (Dir WaspProjectDir) ->
CompileOptions ->
[AS.Decl] ->
IO (Either [CompileError] AS.AppSpec)
constructAppSpec waspDir options decls = do
externalServerCodeFiles <-
ExternalCode.readFiles (CompileOptions.externalServerCodeDirPath options)
externalClientCodeFiles <-
ExternalCode.readFiles (CompileOptions.externalClientCodeDirPath options)
externalSharedCodeFiles <-
ExternalCode.readFiles (CompileOptions.externalSharedCodeDirPath options)
maybeDotEnvServerFile <- findDotEnvServer waspDir
maybeDotEnvClientFile <- findDotEnvClient waspDir
maybeMigrationsDir <- findMigrationsDir waspDir
maybeUserDockerfileContents <- loadUserDockerfileContents waspDir
configFiles <- CF.discoverConfigFiles waspDir G.CF.configFileRelocationMap
let appSpec =
AS.AppSpec
{ AS.decls = decls,
AS.waspProjectDir = waspDir,
AS.externalClientFiles = externalClientCodeFiles,
AS.externalServerFiles = externalServerCodeFiles,
AS.externalSharedFiles = externalSharedCodeFiles,
AS.migrationsDir = maybeMigrationsDir,
AS.dotEnvServerFile = maybeDotEnvServerFile,
AS.dotEnvClientFile = maybeDotEnvClientFile,
AS.isBuild = CompileOptions.isBuild options,
AS.userDockerfileContents = maybeUserDockerfileContents,
AS.configFiles = configFiles
}
return $ case validateAppSpec appSpec of
[] -> Right appSpec
validationErrors -> Left $ map show validationErrors
findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String (Path' Abs File'))
findWaspFile waspDir = do
files <- fst <$> IOUtil.listDirectory waspDir
return $ maybeToEither "Couldn't find a single *.wasp file." $ (waspDir </>) <$> find isWaspFile files
where
isWaspFile path =
".wasp" `isSuffixOf` toFilePath path
&& (length (toFilePath path) > length (".wasp" :: String))
findDotEnvServer :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File'))
findDotEnvServer waspDir = findFileInWaspProjectDir waspDir dotEnvServer
findDotEnvClient :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File'))
findDotEnvClient waspDir = findFileInWaspProjectDir waspDir dotEnvClient
findDotEnv :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File'))
findDotEnv waspDir = findFileInWaspProjectDir waspDir [relfile|.env|]
findFileInWaspProjectDir ::
Path' Abs (Dir WaspProjectDir) ->
Path' (Rel WaspProjectDir) File' ->
IO (Maybe (Path' Abs File'))
findFileInWaspProjectDir waspDir file = do
let fileAbsFp = waspDir </> file
fileExists <- doesFileExist $ toFilePath fileAbsFp
return $ if fileExists then Just fileAbsFp else Nothing
findMigrationsDir ::
Path' Abs (Dir WaspProjectDir) ->
IO (Maybe (Path' Abs (Dir DbMigrationsDir)))
findMigrationsDir waspDir = do
let migrationsAbsPath = waspDir </> dbMigrationsDirInWaspProjectDir
migrationsExists <- doesDirectoryExist $ fromAbsDir migrationsAbsPath
return $ if migrationsExists then Just migrationsAbsPath else Nothing
loadUserDockerfileContents :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe Text)
loadUserDockerfileContents waspDir = do
let dockerfileAbsPath = toFilePath $ waspDir </> [relfile|Dockerfile|]
whenMaybeM (doesFileExist dockerfileAbsPath) $ T.IO.readFile dockerfileAbsPath
compileAndRenderDockerfile :: Path' Abs (Dir WaspProjectDir) -> CompileOptions -> IO (Either [CompileError] Text)
compileAndRenderDockerfile waspDir compileOptions = do
appSpecOrAnalyzerErrors <- analyzeWaspProject waspDir compileOptions
case appSpecOrAnalyzerErrors of
Left errors -> return $ Left errors
Right appSpec -> do
dockerfileOrGeneratorErrors <- DockerGenerator.compileAndRenderDockerfile appSpec
return $ left (map show . toList) dockerfileOrGeneratorErrors
-- | This will run our TS deploy project by passing all args from the Wasp CLI straight through.
-- The TS project is compiled to JS in CI and included in the data dir for the release archive.
-- If the project was not yet built locally (i.e. after they just installed a Wasp version), we do so.
deploy :: FilePath -> Path' Abs (Dir WaspProjectDir) -> [String] -> IO ()
deploy waspExe waspDir cmdArgs = do
waspDataDir <- Data.getAbsDataDirPath
let deployDir = waspDataDir </> [reldir|packages/deploy|]
let nodeModulesDirExists = doesDirectoryExist . toFilePath $ deployDir </> [reldir|node_modules|]
unlessM nodeModulesDirExists $
runCommandAndPrintOutput $ runNodeCommandAsJob deployDir "npm" ["install"] J.Server
let deployScriptArgs = ["dist/index.js"] ++ cmdArgs ++ ["--wasp-exe", waspExe, "--wasp-project-dir", toFilePath waspDir]
-- NOTE: Here we are lying by saying we are running in the J.Server context.
-- TODO: Consider adding a new context for these types of things, like J.Other or J.External.
runCommandAndPrintOutput $ runNodeCommandAsJob deployDir "node" deployScriptArgs J.Server
where
runCommandAndPrintOutput job = do
chan <- newChan
void $ concurrently (printJobMsgsUntilExitReceived chan) (job chan)

56
waspc/src/Wasp/Project.hs Normal file
View File

@ -0,0 +1,56 @@
-- | "Project" here stands for a Wasp source project, and this module offers
-- logic for operating on and processing a Wasp source project, as a whole.
module Wasp.Project
( WaspProjectDir,
compile,
CompileError,
CompileWarning,
analyzeWaspProject,
compileAndRenderDockerfile,
)
where
import Control.Arrow (ArrowChoice (left))
import Data.List.NonEmpty (toList)
import Data.Maybe (maybeToList)
import Data.Text (Text)
import StrongPath (Abs, Dir, Path')
import qualified Wasp.AppSpec as AS
import Wasp.CompileOptions (CompileOptions (generatorWarningsFilter), sendMessage)
import qualified Wasp.Generator as Generator
import qualified Wasp.Generator.DockerGenerator as DockerGenerator
import Wasp.Project.Analyze (analyzeWaspProject)
import Wasp.Project.Common (CompileError, CompileWarning, WaspProjectDir)
import qualified Wasp.Project.Env as Project.Env
compile ::
Path' Abs (Dir WaspProjectDir) ->
Path' Abs (Dir Generator.ProjectRootDir) ->
CompileOptions ->
IO ([CompileWarning], [CompileError])
compile waspDir outDir options = do
compileWarningsAndErrors <-
analyzeWaspProject waspDir options >>= \case
Left analyzerErrors -> return ([], analyzerErrors)
Right appSpec -> generateCode appSpec outDir options
dotEnvWarnings <- maybeToList <$> Project.Env.warnIfTheDotEnvPresent waspDir
return $ (dotEnvWarnings, []) <> compileWarningsAndErrors
generateCode ::
AS.AppSpec ->
Path' Abs (Dir Generator.ProjectRootDir) ->
CompileOptions ->
IO ([CompileError], [CompileWarning])
generateCode appSpec outDir options = do
(generatorWarnings, generatorErrors) <- Generator.writeWebAppCode appSpec outDir (sendMessage options)
let filteredWarnings = generatorWarningsFilter options generatorWarnings
return (show <$> filteredWarnings, show <$> generatorErrors)
compileAndRenderDockerfile :: Path' Abs (Dir WaspProjectDir) -> CompileOptions -> IO (Either [CompileError] Text)
compileAndRenderDockerfile waspDir compileOptions = do
appSpecOrAnalyzerErrors <- analyzeWaspProject waspDir compileOptions
case appSpecOrAnalyzerErrors of
Left errors -> return $ Left errors
Right appSpec -> do
dockerfileOrGeneratorErrors <- DockerGenerator.compileAndRenderDockerfile appSpec
return $ Control.Arrow.left (map show . toList) dockerfileOrGeneratorErrors

View File

@ -0,0 +1,90 @@
module Wasp.Project.Analyze
( analyzeWaspProject,
)
where
import Control.Arrow (ArrowChoice (left))
import Control.Monad.Except (ExceptT (ExceptT), runExceptT)
import Data.List (find, isSuffixOf)
import StrongPath (Abs, Dir, File', Path', toFilePath, (</>))
import qualified Wasp.Analyzer as Analyzer
import Wasp.Analyzer.AnalyzeError (getErrorMessageAndCtx)
import qualified Wasp.AppSpec as AS
import Wasp.AppSpec.Valid (validateAppSpec)
import Wasp.CompileOptions (CompileOptions)
import qualified Wasp.CompileOptions as CompileOptions
import qualified Wasp.ConfigFile as CF
import Wasp.Error (showCompilerErrorForTerminal)
import qualified Wasp.ExternalCode as ExternalCode
import qualified Wasp.Generator.ConfigFile as G.CF
import Wasp.Project.Common (CompileError, WaspProjectDir)
import Wasp.Project.Db (makeDevDatabaseUrl)
import Wasp.Project.Db.Migrations (findMigrationsDir)
import Wasp.Project.Deployment (loadUserDockerfileContents)
import Wasp.Project.Env (readDotEnvClient, readDotEnvServer)
import Wasp.Util (maybeToEither)
import qualified Wasp.Util.IO as IOUtil
analyzeWaspProject ::
Path' Abs (Dir WaspProjectDir) ->
CompileOptions ->
IO (Either [CompileError] AS.AppSpec)
analyzeWaspProject waspDir options = runExceptT $ do
waspFilePath <- ExceptT $ Control.Arrow.left pure <$> findWaspFile waspDir
declarations <- ExceptT $ Control.Arrow.left pure <$> analyzeWaspFileContent waspFilePath
ExceptT $ constructAppSpec waspDir options declarations
analyzeWaspFileContent :: Path' Abs File' -> IO (Either CompileError [AS.Decl])
analyzeWaspFileContent waspFilePath = do
waspFileContent <- IOUtil.readFile waspFilePath
let declsOrAnalyzeError = Analyzer.analyze waspFileContent
return $
Control.Arrow.left
(showCompilerErrorForTerminal (waspFilePath, waspFileContent) . getErrorMessageAndCtx)
declsOrAnalyzeError
constructAppSpec ::
Path' Abs (Dir WaspProjectDir) ->
CompileOptions ->
[AS.Decl] ->
IO (Either [CompileError] AS.AppSpec)
constructAppSpec waspDir options decls = do
externalServerCodeFiles <-
ExternalCode.readFiles (CompileOptions.externalServerCodeDirPath options)
externalClientCodeFiles <-
ExternalCode.readFiles (CompileOptions.externalClientCodeDirPath options)
externalSharedCodeFiles <-
ExternalCode.readFiles (CompileOptions.externalSharedCodeDirPath options)
maybeMigrationsDir <- findMigrationsDir waspDir
maybeUserDockerfileContents <- loadUserDockerfileContents waspDir
configFiles <- CF.discoverConfigFiles waspDir G.CF.configFileRelocationMap
let devDbUrl = makeDevDatabaseUrl waspDir decls
serverEnvVars <- readDotEnvServer waspDir
clientEnvVars <- readDotEnvClient waspDir
let appSpec =
AS.AppSpec
{ AS.decls = decls,
AS.waspProjectDir = waspDir,
AS.externalClientFiles = externalClientCodeFiles,
AS.externalServerFiles = externalServerCodeFiles,
AS.externalSharedFiles = externalSharedCodeFiles,
AS.migrationsDir = maybeMigrationsDir,
AS.devEnvVarsServer = serverEnvVars,
AS.devEnvVarsClient = clientEnvVars,
AS.isBuild = CompileOptions.isBuild options,
AS.userDockerfileContents = maybeUserDockerfileContents,
AS.configFiles = configFiles,
AS.devDatabaseUrl = devDbUrl
}
return $ case validateAppSpec appSpec of
[] -> Right appSpec
validationErrors -> Left $ map show validationErrors
findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String (Path' Abs File'))
findWaspFile waspDir = do
files <- fst <$> IOUtil.listDirectory waspDir
return $ maybeToEither "Couldn't find a single *.wasp file." $ (waspDir </>) <$> find isWaspFile files
where
isWaspFile path =
".wasp" `isSuffixOf` toFilePath path
&& (length (toFilePath path) > length (".wasp" :: String))

View File

@ -0,0 +1,25 @@
module Wasp.Project.Common
( findFileInWaspProjectDir,
CompileError,
CompileWarning,
WaspProjectDir,
)
where
import StrongPath (Abs, Dir, File', Path', Rel, toFilePath, (</>))
import System.Directory (doesFileExist)
data WaspProjectDir -- Root dir of Wasp project, containing source files.
type CompileError = String
type CompileWarning = String
findFileInWaspProjectDir ::
Path' Abs (Dir WaspProjectDir) ->
Path' (Rel WaspProjectDir) File' ->
IO (Maybe (Path' Abs File'))
findFileInWaspProjectDir waspDir file = do
let fileAbsFp = waspDir </> file
fileExists <- doesFileExist $ toFilePath fileAbsFp
return $ if fileExists then Just fileAbsFp else Nothing

View File

@ -0,0 +1,23 @@
module Wasp.Project.Db
( makeDevDatabaseUrl,
databaseUrlEnvVarName,
)
where
import StrongPath (Abs, Dir, Path')
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Db as AS.App.Db
import Wasp.Project.Common (WaspProjectDir)
import qualified Wasp.Project.Db.Dev.Postgres as DevPostgres
makeDevDatabaseUrl :: Path' Abs (Dir WaspProjectDir) -> [AS.Decl] -> Maybe String
makeDevDatabaseUrl waspProjectDir decls = do
(appName, app) <- AS.getApp decls
dbSystem <- AS.App.Db.system =<< AS.App.db app
case dbSystem of
AS.App.Db.SQLite -> Nothing
AS.App.Db.PostgreSQL -> Just $ DevPostgres.makeDevConnectionUrl waspProjectDir appName
databaseUrlEnvVarName :: String
databaseUrlEnvVarName = "DATABASE_URL"

View File

@ -0,0 +1,25 @@
-- | This module and modules under it capture what Wasp knows about its dev database and running it.
module Wasp.Project.Db.Dev
( makeDevDbUniqueId,
)
where
import Data.Char (isAsciiLower, isAsciiUpper, isDigit)
import StrongPath (Abs, Dir, Path', fromAbsDir)
import Wasp.Project.Common (WaspProjectDir)
import qualified Wasp.Util as U
-- Returns a unique id that can be used for global identification of dev db tied to
-- a specific Wasp project.
-- Id is no longer than 30 chars, all of them ascii letters, numbers, hyphen or underscore.
-- It is designed this way to make it easily usable in many scenarios.
-- It contains app name (or big part of it), to make it also readable for humans.
-- It is not resistant to Wasp project moving or being renamed, and will change in that case.
-- TODO: Consider making this more general, make it a unique id of a project that can be used to
-- track (global) resources. So it would be not just for the db, but for anything.
makeDevDbUniqueId :: Path' Abs (Dir WaspProjectDir) -> String -> String
makeDevDbUniqueId waspProjectDir appName = take 19 sanitizedAppName <> "-" <> take 10 projectPathHash
where
projectPathHash = U.hexToString $ U.checksumFromString $ fromAbsDir waspProjectDir
sanitizedAppName = filter isSafeChar appName
isSafeChar c = isAsciiLower c || isAsciiUpper c || isDigit c || c == '_' || c == '-'

View File

@ -0,0 +1,38 @@
-- | This module captures how Wasp runs a PostgreSQL dev database.
module Wasp.Project.Db.Dev.Postgres
( defaultDevUser,
makeDevDbName,
defaultDevPass,
defaultDevPort,
makeDevConnectionUrl,
)
where
import StrongPath (Abs, Dir, Path')
import Wasp.Db.Postgres (makeConnectionUrl, postgresMaxDbNameLength)
import Wasp.Project.Common (WaspProjectDir)
import Wasp.Project.Db.Dev (makeDevDbUniqueId)
defaultDevUser :: String
defaultDevUser = "postgresWaspDevUser"
defaultDevPass :: String
defaultDevPass = "postgresWaspDevPass"
-- | Returns a db name that is unique for this Wasp project.
-- It depends on projects path and name, so if any of those change,
-- the db name will also change.
makeDevDbName :: Path' Abs (Dir WaspProjectDir) -> String -> String
makeDevDbName waspProjectDir appName =
-- We use makeDevDbUniqueId to construct a db name instead of a hardcoded value like "waspDevDb"
-- in order to avoid the situation where one Wasp app accidentally connects to a db that another
-- Wasp app has started. This way db name is unique for the specific Wasp app, and another Wasp app
-- can't connect to it by accident.
take postgresMaxDbNameLength $ makeDevDbUniqueId waspProjectDir appName
defaultDevPort :: Int
defaultDevPort = 5432 -- 5432 is default port for PostgreSQL db.
makeDevConnectionUrl :: Path' Abs (Dir WaspProjectDir) -> String -> String
makeDevConnectionUrl waspProjectDir appName =
makeConnectionUrl defaultDevUser defaultDevPass defaultDevPort $ makeDevDbName waspProjectDir appName

View File

@ -0,0 +1,23 @@
module Wasp.Project.Db.Migrations
( DbMigrationsDir,
dbMigrationsDirInWaspProjectDir,
findMigrationsDir,
)
where
import StrongPath (Abs, Dir, Path', Rel, fromAbsDir, reldir, (</>))
import System.Directory (doesDirectoryExist)
import Wasp.Project.Common (WaspProjectDir)
data DbMigrationsDir
dbMigrationsDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir DbMigrationsDir)
dbMigrationsDirInWaspProjectDir = [reldir|migrations|]
findMigrationsDir ::
Path' Abs (Dir WaspProjectDir) ->
IO (Maybe (Path' Abs (Dir DbMigrationsDir)))
findMigrationsDir waspDir = do
let migrationsAbsPath = waspDir </> dbMigrationsDirInWaspProjectDir
migrationsExists <- doesDirectoryExist $ fromAbsDir migrationsAbsPath
return $ if migrationsExists then Just migrationsAbsPath else Nothing

View File

@ -0,0 +1,44 @@
module Wasp.Project.Deployment
( loadUserDockerfileContents,
deploy,
)
where
import Control.Concurrent (newChan)
import Control.Concurrent.Async (concurrently)
import Control.Monad.Except (void)
import Control.Monad.Extra (whenMaybeM)
import Data.Text (Text)
import qualified Data.Text.IO as T.IO
import StrongPath (Abs, Dir, Path', reldir, relfile, toFilePath, (</>))
import System.Directory (doesDirectoryExist, doesFileExist)
import qualified Wasp.Data as Data
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.IO (printJobMsgsUntilExitReceived)
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
import Wasp.Project.Common (WaspProjectDir)
import Wasp.Util (unlessM)
loadUserDockerfileContents :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe Text)
loadUserDockerfileContents waspDir = do
let dockerfileAbsPath = toFilePath $ waspDir </> [relfile|Dockerfile|]
whenMaybeM (doesFileExist dockerfileAbsPath) $ T.IO.readFile dockerfileAbsPath
-- | This will run our TS deploy project by passing all args from the Wasp CLI straight through.
-- The TS project is compiled to JS in CI and included in the data dir for the release archive.
-- If the project was not yet built locally (i.e. after they just installed a Wasp version), we do so.
deploy :: FilePath -> Path' Abs (Dir WaspProjectDir) -> [String] -> IO ()
deploy waspExe waspDir cmdArgs = do
waspDataDir <- Data.getAbsDataDirPath
let deployDir = waspDataDir </> [reldir|packages/deploy|]
let nodeModulesDirExists = doesDirectoryExist . toFilePath $ deployDir </> [reldir|node_modules|]
unlessM nodeModulesDirExists $
runCommandAndPrintOutput $ runNodeCommandAsJob deployDir "npm" ["install"] J.Server
let deployScriptArgs = ["dist/index.js"] ++ cmdArgs ++ ["--wasp-exe", waspExe, "--wasp-project-dir", toFilePath waspDir]
-- NOTE: Here we are lying by saying we are running in the J.Server context.
-- TODO: Consider adding a new context for these types of things, like J.Other or J.External.
runCommandAndPrintOutput $ runNodeCommandAsJob deployDir "node" deployScriptArgs J.Server
where
runCommandAndPrintOutput job = do
chan <- newChan
void $ concurrently (printJobMsgsUntilExitReceived chan) (job chan)

View File

@ -0,0 +1,45 @@
module Wasp.Project.Env
( readDotEnvServer,
readDotEnvClient,
warnIfTheDotEnvPresent,
dotEnvServer,
dotEnvClient,
)
where
import StrongPath (Abs, Dir, File', Path', Rel, relfile)
import Wasp.Env (EnvVar, parseDotEnvFile)
import Wasp.Project.Common (CompileWarning, WaspProjectDir, findFileInWaspProjectDir)
dotEnvServer :: Path' (Rel WaspProjectDir) File'
dotEnvServer = [relfile|.env.server|]
dotEnvClient :: Path' (Rel WaspProjectDir) File'
dotEnvClient = [relfile|.env.client|]
readDotEnvServer :: Path' Abs (Dir WaspProjectDir) -> IO [EnvVar]
readDotEnvServer waspDir = readDotEnvFileInWaspProjectDir waspDir dotEnvServer
readDotEnvClient :: Path' Abs (Dir WaspProjectDir) -> IO [EnvVar]
readDotEnvClient waspDir = readDotEnvFileInWaspProjectDir waspDir dotEnvServer
-- | Checks if .env exists in wasp dir, and produces a warning if so.
-- We have this function because Wasp doesn't use ".env", but still user
-- might assume it does and then bother quite a bit trying to figure it out,
-- so this way we warn if they assume so.
warnIfTheDotEnvPresent :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe CompileWarning)
warnIfTheDotEnvPresent waspDir = (warningMessage <$) <$> findFileInWaspProjectDir waspDir [relfile|.env|]
where
warningMessage = "Wasp .env files should be named .env.server or .env.client, depending on their use."
-- Reads specified dotenv file and returns its values.
-- If file doesn't exist, returns an empty list.
-- If file can't be parsed, it will crash with an error.
readDotEnvFileInWaspProjectDir ::
Path' Abs (Dir WaspProjectDir) ->
Path' (Rel WaspProjectDir) File' ->
IO [(String, String)]
readDotEnvFileInWaspProjectDir waspDir envFileInWaspDir = do
findFileInWaspProjectDir waspDir envFileInWaspDir >>= \case
Nothing -> return []
Just envFile -> parseDotEnvFile envFile

View File

@ -32,11 +32,12 @@ module Wasp.Util
orIfNothingM,
kebabToCamelCase,
maybeToEither,
whenM,
)
where
import Control.Applicative (liftA2)
import Control.Monad (unless)
import Control.Monad (unless, when)
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Data.Aeson as Aeson
import qualified Data.ByteString as B
@ -180,6 +181,9 @@ infixr 5 <:>
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM p x y = p >>= \b -> if b then x else y
whenM :: Monad m => m Bool -> m () -> m ()
whenM ma mb = ma >>= (`when` mb)
unlessM :: Monad m => m Bool -> m () -> m ()
unlessM ma mb = ma >>= (`unless` mb)

View File

@ -0,0 +1,81 @@
module Wasp.Util.Network.Socket
( checkIfPortIsAcceptingConnections,
checkIfPortIsInUse,
checkIfPortCanBeOpened,
makeSocketAddress,
makeLocalHostSocketAddress,
IPv4,
)
where
import Data.Word (Word8)
import Foreign.C.Error (Errno (..), eADDRINUSE, eCONNREFUSED)
import GHC.IO.Exception (IOException (..))
import qualified Network.Socket as S
import UnliftIO.Exception (bracket, throwIO, try)
-- | Tests if port is accepting connections.
-- Does so by trying to connect via socket to it (connection is closed immediately).
-- It returns True if connection succeeds, or False if connection is refused
-- (because port is not opened, nobody is listening on it).
-- Rethrows connection exceptions in all other cases (e.g. when the host
-- is unroutable).
checkIfPortIsAcceptingConnections :: S.SockAddr -> IO Bool
checkIfPortIsAcceptingConnections sockAddr = do
bracket createSocket S.close' $ \sock ->
try
( do
S.connect sock sockAddr
)
>>= \case
Right () -> return True
Left e ->
if isConnRefusedException e
then return False
else throwIO e
where
createSocket = createIPv4TCPSocket
isConnRefusedException e = (Errno <$> ioe_errno e) == Just eCONNREFUSED
-- | True if port is in use, False if it is free, exception in all other cases.
checkIfPortIsInUse :: S.SockAddr -> IO Bool
checkIfPortIsInUse = (not <$>) . checkIfPortCanBeOpened
-- | Tests if port can be opened.
-- Does so by trying to bind a socket to it (and then closing it immediately).
-- Returns True if it can be opened, False if it is already in use, and throws
-- an exception in all other cases (e.g. when the host is unroutable).
checkIfPortCanBeOpened :: S.SockAddr -> IO Bool
checkIfPortCanBeOpened sockAddr = do
bracket createSocket S.close' $ \sock ->
try
( do
S.bind sock sockAddr
S.listen sock queueLength
)
>>= \case
Right () -> return True
Left e ->
if isAddrInUseException e
then return False
else throwIO e
where
createSocket = do
sock <- createIPv4TCPSocket
S.setSocketOption sock S.ReuseAddr 1 -- Connect even if port is in TIME_WAIT state.
return sock
queueLength = 1
isAddrInUseException e = (Errno <$> ioe_errno e) == Just eADDRINUSE
createIPv4TCPSocket :: IO S.Socket
createIPv4TCPSocket = S.socket S.AF_INET S.Stream S.defaultProtocol
-- | Creates a socket address from host IP and port number.
-- > makeSocketAddress (127,0,0,1) 8000
makeSocketAddress :: IPv4 -> S.PortNumber -> S.SockAddr
makeSocketAddress hostIp port = S.SockAddrInet port $ S.tupleToHostAddress hostIp
makeLocalHostSocketAddress :: S.PortNumber -> S.SockAddr
makeLocalHostSocketAddress = makeSocketAddress (127, 0, 0, 1)
type IPv4 = (Word8, Word8, Word8, Word8)

View File

@ -199,10 +199,11 @@ spec_AppSpecValid = do
AS.externalSharedFiles = [],
AS.isBuild = False,
AS.migrationsDir = Nothing,
AS.dotEnvServerFile = Nothing,
AS.dotEnvClientFile = Nothing,
AS.devEnvVarsClient = [],
AS.devEnvVarsServer = [],
AS.userDockerfileContents = Nothing,
AS.configFiles = []
AS.configFiles = [],
AS.devDatabaseUrl = Nothing
}
basicPage =

View File

@ -0,0 +1,27 @@
module Generator.FileDraft.CopyAndModifyTextFileDraftTest where
import Fixtures (systemSPRoot)
import qualified Generator.MockWriteableMonad as Mock
import StrongPath (parent, reldir, relfile, toFilePath, (</>))
import Test.Tasty.Hspec
import Wasp.Generator.FileDraft
spec_CopyAndModifyTextFileDraft :: Spec
spec_CopyAndModifyTextFileDraft = do
describe "write" $ do
it "Creates new file by copying existing text file and applying modifications to it" $ do
let mock = write dstDir fileDraft
let mockLogs = Mock.getMockLogs mock Mock.defaultMockConfig
Mock.createDirectoryIfMissing_calls mockLogs
`shouldBe` [(True, toFilePath $ parent expectedDstPath)]
Mock.readFileAsText_calls mockLogs
`shouldBe` [toFilePath expectedSrcPath]
Mock.writeFileFromText_calls mockLogs
`shouldBe` [(toFilePath expectedDstPath, "First line\nMock text file content")]
where
dstDir = systemSPRoot </> [reldir|a/b|]
dstPath = [relfile|c/d/dst.txt|]
srcPath = systemSPRoot </> [relfile|e/src.txt|]
fileDraft = createCopyAndModifyTextFileDraft dstPath srcPath ("First line\n" <>)
expectedSrcPath = srcPath
expectedDstPath = dstDir </> dstPath

View File

@ -33,18 +33,24 @@ defaultMockConfig =
getTemplateFileAbsPath_impl = \path -> systemSPRoot </> [reldir|mock/templates/dir|] </> path,
compileAndRenderTemplate_impl = \_ _ -> pack "Mock template content",
doesFileExist_impl = const True,
doesDirectoryExist_impl = const True
doesDirectoryExist_impl = const True,
readFileAsText_impl = \_ -> pack "Mock text file content"
}
getMockLogs :: MockWriteableMonad a -> MockWriteableMonadConfig -> MockWriteableMonadLogs
getMockLogs mock config = fst $ execState (unMockWriteableMonad mock) (emptyLogs, config)
where
emptyLogs = MockWriteableMonadLogs [] [] [] [] [] [] [] []
emptyLogs = MockWriteableMonadLogs [] [] [] [] [] [] [] [] []
instance WriteableMonad MockWriteableMonad where
writeFileFromText dstPath text = MockWriteableMonad $ do
modifyLogs (writeFileFromText_addCall dstPath text)
readFileAsText srcPath = MockWriteableMonad $ do
modifyLogs (readFileAsText_addCall srcPath)
(_, config) <- get
return $ readFileAsText_impl config srcPath
getTemplatesDirAbsPath = MockWriteableMonad $ do
modifyLogs getTemplatesDirAbsPath_addCall
(_, config) <- get
@ -95,6 +101,7 @@ newtype MockWriteableMonad a = MockWriteableMonad
data MockWriteableMonadLogs = MockWriteableMonadLogs
{ writeFileFromText_calls :: [(FilePath, Text)],
readFileAsText_calls :: [FilePath],
getTemplatesDirAbsPath_calls :: [()],
createDirectoryIfMissing_calls :: [(Bool, FilePath)],
copyFile_calls :: [(FilePath, FilePath)],
@ -109,13 +116,18 @@ data MockWriteableMonadConfig = MockWriteableMonadConfig
getTemplateFileAbsPath_impl :: forall a. Path' (Rel TemplatesDir) (File a) -> Path' Abs (File a),
compileAndRenderTemplate_impl :: forall a. Path' (Rel TemplatesDir) (File a) -> Aeson.Value -> Text,
doesFileExist_impl :: FilePath -> Bool,
doesDirectoryExist_impl :: FilePath -> Bool
doesDirectoryExist_impl :: FilePath -> Bool,
readFileAsText_impl :: FilePath -> Text
}
writeFileFromText_addCall :: FilePath -> Text -> MockWriteableMonadLogs -> MockWriteableMonadLogs
writeFileFromText_addCall path text logs =
logs {writeFileFromText_calls = (path, text) : writeFileFromText_calls logs}
readFileAsText_addCall :: FilePath -> MockWriteableMonadLogs -> MockWriteableMonadLogs
readFileAsText_addCall path logs =
logs {readFileAsText_calls = path : readFileAsText_calls logs}
getTemplatesDirAbsPath_addCall :: MockWriteableMonadLogs -> MockWriteableMonadLogs
getTemplatesDirAbsPath_addCall logs =
logs {getTemplatesDirAbsPath_calls = () : getTemplatesDirAbsPath_calls logs}

View File

@ -9,6 +9,7 @@ import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Wasp as AS.Wasp
import qualified Wasp.AppSpec.Core.Decl as AS.Decl
import Wasp.Generator.FileDraft
import qualified Wasp.Generator.FileDraft.CopyAndModifyTextFileDraft as CMTextFD
import qualified Wasp.Generator.FileDraft.CopyDirFileDraft as CopyDirFD
import qualified Wasp.Generator.FileDraft.CopyFileDraft as CopyFD
import qualified Wasp.Generator.FileDraft.TemplateFileDraft as TmplFD
@ -48,10 +49,11 @@ spec_WebAppGenerator = do
AS.externalSharedFiles = [],
AS.isBuild = False,
AS.migrationsDir = Nothing,
AS.dotEnvServerFile = Nothing,
AS.dotEnvClientFile = Nothing,
AS.devEnvVarsServer = [],
AS.devEnvVarsClient = [],
AS.userDockerfileContents = Nothing,
AS.configFiles = []
AS.configFiles = [],
AS.devDatabaseUrl = Nothing
}
describe "genWebApp" $ do
@ -101,3 +103,4 @@ getFileDraftDstPath (FileDraftTemplateFd fd) = SP.toFilePath $ TmplFD._dstPath f
getFileDraftDstPath (FileDraftCopyFd fd) = SP.toFilePath $ CopyFD._dstPath fd
getFileDraftDstPath (FileDraftCopyDirFd fd) = SP.toFilePath $ CopyDirFD._dstPath fd
getFileDraftDstPath (FileDraftTextFd fd) = SP.toFilePath $ TextFD._dstPath fd
getFileDraftDstPath (FileDraftCopyAndModifyTextFd fd) = SP.toFilePath $ CMTextFD._dstPath fd

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -Wno-orphans #-}
module Generator.WriteFileDraftsTest where
import Data.Bifunctor (Bifunctor (first))
@ -34,6 +36,14 @@ spec_WriteDuplicatedDstFileDrafts =
let fileDrafts = map FileDraftTextFd (genMockTextFileDrafts 2)
in (return $! assertDstPathsAreUnique fileDrafts) `shouldReturn` ()
-- NOTE: Very weak show function, but it is good enough for the tests below.
instance Show FileDraft where
show fd = "FileDraft {dstPath = " ++ show (getDstPath fd) ++ "}"
-- NOTE: Very weak eq function, but it is good enough for the tests below.
instance Eq FileDraft where
fd1 == fd2 = getDstPath fd1 == getDstPath fd2
spec_WriteFileDrafts :: Spec
spec_WriteFileDrafts =
describe "fileDraftsToWriteAndFilesToDelete" $ do

View File

@ -1,7 +1,7 @@
module WaspignoreFileTest where
import Test.Tasty.Hspec
import Test.Tasty.QuickCheck (property)
import Test.Tasty.QuickCheck (arbitraryPrintableChar, forAll, listOf, property)
import Wasp.WaspignoreFile (ignores, parseWaspignoreFile)
spec_IgnoreFile :: Spec
@ -15,26 +15,31 @@ spec_IgnoreFile = do
it "When given a blank input, should match only '.waspignore'" $ do
let ignoreFile = parseWaspignoreFile ""
property $ \fp ->
if fp == ".waspignore"
then ignoreFile `ignores` fp
else not $ ignoreFile `ignores` fp
property $
forAll (listOf arbitraryPrintableChar) $ \fp ->
if fp == ".waspignore"
then ignoreFile `ignores` fp
else not $ ignoreFile `ignores` fp
it "When given a comment as the only line, should match only '.waspignore'" $ do
let ignoreFile = parseWaspignoreFile "# test comment"
property $ \fp ->
if fp == ".waspignore"
then ignoreFile `ignores` fp
else not $ ignoreFile `ignores` fp
property $
forAll (listOf arbitraryPrintableChar) $ \fp ->
if fp == ".waspignore"
then ignoreFile `ignores` fp
else not $ ignoreFile `ignores` fp
it "When the only difference between two files is a comment, the files should match the same strings" $ do
let comment = "\n# test comment"
property $ \pat fp ->
(parseWaspignoreFile pat `ignores` fp)
== (parseWaspignoreFile (pat ++ comment) `ignores` fp)
property $
forAll (listOf arbitraryPrintableChar) $ \pat fp ->
(parseWaspignoreFile pat `ignores` fp)
== (parseWaspignoreFile (pat ++ comment) `ignores` fp)
it "When given 2 patterns, should match the path if either of the patterns match" $ do
let pat1 = parseWaspignoreFile "a"
let pat2 = parseWaspignoreFile "b"
let patBoth = parseWaspignoreFile "a\nb"
property $ \fp -> patBoth `ignores` fp == (pat1 `ignores` fp || pat2 `ignores` fp)
property $
forAll (listOf arbitraryPrintableChar) $ \fp ->
patBoth `ignores` fp == (pat1 `ignores` fp || pat2 `ignores` fp)

View File

@ -6,7 +6,7 @@ cabal-version: 2.4
-- Consider using hpack, or maybe even hpack-dhall.
name: waspc
version: 0.9.0
version: 0.9.1
description: Please see the README on GitHub at <https://github.com/wasp-lang/wasp/waspc#readme>
homepage: https://github.com/wasp-lang/wasp/waspc#readme
bug-reports: https://github.com/wasp-lang/wasp/issues
@ -124,6 +124,8 @@ library
, array ^>= 0.5.4
, deepseq ^>= 1.4.4
, extra ^>= 1.7.10
, dotenv ^>= 0.10.0
, network ^>= 3.1.2
other-modules: Paths_waspc
exposed-modules:
FilePath.Extra
@ -205,11 +207,12 @@ library
Wasp.AppSpec.Route
Wasp.AppSpec.Valid
Wasp.AppSpec.Util
Wasp.Common
Wasp.CompileOptions
Wasp.ConfigFile
Wasp.Data
Wasp.Db.Postgres
Wasp.Error
Wasp.Env
Wasp.ExternalCode
Wasp.JsImport
Wasp.Generator
@ -229,6 +232,7 @@ library
Wasp.Generator.FileDraft.CopyFileDraft
Wasp.Generator.FileDraft.TemplateFileDraft
Wasp.Generator.FileDraft.TextFileDraft
Wasp.Generator.FileDraft.CopyAndModifyTextFileDraft
Wasp.Generator.FileDraft.Writeable
Wasp.Generator.FileDraft.WriteableMonad
Wasp.Generator.Job
@ -273,13 +277,22 @@ library
Wasp.Generator.WebAppGenerator.Setup
Wasp.Generator.WebAppGenerator.Start
Wasp.Generator.WriteFileDrafts
Wasp.Lib
Wasp.Project
Wasp.Project.Analyze
Wasp.Project.Common
Wasp.Project.Db
Wasp.Project.Db.Migrations
Wasp.Project.Db.Dev
Wasp.Project.Db.Dev.Postgres
Wasp.Project.Deployment
Wasp.Project.Env
Wasp.NpmDependency
Wasp.Psl.Ast.Model
Wasp.Psl.Generator.Model
Wasp.Psl.Parser.Model
Wasp.SemanticVersion
Wasp.Util
Wasp.Util.Network.Socket
Wasp.Util.Control.Monad
Wasp.Util.Fib
Wasp.Util.IO
@ -359,11 +372,14 @@ library cli-lib
Wasp.Cli.Command.CreateNewProject
Wasp.Cli.Command.Db
Wasp.Cli.Command.Db.Migrate
Wasp.Cli.Command.Db.Reset
Wasp.Cli.Command.Db.Studio
Wasp.Cli.Command.Deps
Wasp.Cli.Command.Deploy
Wasp.Cli.Command.Dockerfile
Wasp.Cli.Command.Info
Wasp.Cli.Command.Start
Wasp.Cli.Command.Start.Db
Wasp.Cli.Command.Telemetry
Wasp.Cli.Command.Telemetry.Common
Wasp.Cli.Command.Telemetry.Project
@ -435,6 +451,7 @@ test-suite waspc-test
Generator.DbGeneratorTest
Generator.ExternalCodeGenerator.JsTest
Generator.FileDraft.CopyFileDraftTest
Generator.FileDraft.CopyAndModifyTextFileDraftTest
Generator.FileDraft.TemplateFileDraftTest
Generator.MockWriteableMonad
Generator.WebAppGeneratorTest
@ -483,6 +500,7 @@ test-suite e2e-test
main-is: Main.hs
build-tool-depends: waspc:wasp-cli
build-depends:
, waspc
, aeson
, directory
, base

View File

@ -21,8 +21,9 @@ COMMANDS
uninstall Removes Wasp from your system.
IN PROJECT
start Runs Wasp app in development mode, watching for file changes.
start db Starts managed development database for you.
db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.
clean Deletes all generated code and other cached artifacts. Wasp equivalent of 'have you tried closing and opening it again?'.
clean Deletes all generated code and other cached artifacts. Wasp equivalent of 'have you tried closing and opening it again?'.
build Generates full web app code, ready for deployment. Use when deploying or ejecting.
deploy Deploys your Wasp app to cloud hosting providers.
telemetry Prints telemetry status.
@ -80,7 +81,7 @@ To setup Bash completion, execute `wasp completion` and follow the instructions.
### In project
- `wasp start` runs Wasp app in development mode. It opens a browser tab with your application running, and watches for any changes to .wasp or files in `src/` to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.
- `wasp start db` starts the database for you. This can be very handy, since you don't need to spin up your own database or provide its connection URL to the Wasp app!
- `wasp clean` deletes all generated code and other cached artifacts. If using SQlite, it also deletes the SQlite database. It is the Wasp equivalent to "try shutting it down and turning back on".
```

View File

@ -1717,7 +1717,7 @@ Since environmental variables are usually different for server-side and client a
`.env.server` and `.env.client` files should not be commited to the version control - we already ignore it by default in the .gitignore file we generate when you create a new Wasp project via `wasp new` cli command.
Variables are defined in `.env.server` or `env.client` files in the form of `NAME=VALUE`, for example:
Variables are defined in `.env.server` or `.env.client` files in the form of `NAME=VALUE`, for example:
```
DATABASE_URL=postgresql://localhost:5432
MY_VAR=somevalue
@ -1754,29 +1754,20 @@ Default database is `SQLite`, since it is great for getting started with a new p
Check below for more details on how to migrate from SQLite to PostgreSQL.
### PostgreSQL
When using `PostgreSQL` as your database (`app: { db: { system: PostgreSQL } }`), you will need to spin up a postgres database on your own so it runs during development (when running `wasp start` or doing `wasp db ...` commands) and you will need to provide Wasp with `DATABASE_URL` environment variable that Wasp will use to connect to it.
When using `PostgreSQL` as your database (`app: { db: { system: PostgreSQL } }`), you will need to make sure you have a postgres database running during development (when running `wasp start` or doing `wasp db ...` commands).
One of the easiest ways to run a PostgreSQL database on your own is by spinning up [postgres docker](https://hub.docker.com/_/postgres) container when you need it with the following shell command:
```
docker run \
--rm \
--publish 5432:5432 \
-v my-app-data:/var/lib/postgresql/data \
--env POSTGRES_PASSWORD=devpass1234 \
postgres
```
To help with this, Wasp provides `wasp start db` that starts the default db for you. Your Wasp app will automatically connect to it once you have it running via `wasp start db`, no additional configuration is needed. This command relies on Docker being installed on your machine.
:::note
The password you provide via `POSTGRES_PASSWORD` is relevant only for the first time when you run that docker command, when database is set up for the first time. Consequent runs will ignore the value of `POSTGRES_PASSWORD` and will just use the password that was initially set. This is just how postgres docker works.
:::
#### Custom database
The easiest way to provide the needed `DATABASE_URL` environment variable is by adding the following line to the [.env.server](https://wasp-lang.dev/docs/language/features#env) file in the root dir of your Wasp project (if that file doesn't yet exist, create it):
```
DATABASE_URL=postgresql://postgres:devpass1234@localhost:5432/postgres
```
If instead of using `wasp start db` you would rather connect to some other database, you will need to provide Wasp with `DATABASE_URL` environment variable that Wasp will use to connect to it.
The easiest way to provide the needed `DATABASE_URL` environment variable is by adding it to the [.env.server](https://wasp-lang.dev/docs/language/features#env) file in the root dir of your Wasp project (if that file doesn't yet exist, create it).
### Migrating from SQLite to PostgreSQL
To run Wasp app in production, you will need to switch from `SQLite` to `PostgreSQL`.
1. Set `app.db.system` to `PostgreSQL` and set `DATABASE_URL` env var accordingly (as described [above](/docs/language/features#postgresql)).
2. Delete old migrations, since they are SQLite migrations and can't be used with PostgreSQL: `rm -r migrations/`.
3. Run `wasp db migrate-dev` to apply new changes and create new, initial migration. You will need to have your postgres database running while doing this (check [above](/docs/language/features#postgresql) for easy way to get it running).
1. Set `app.db.system` to `PostgreSQL`.
3. Delete old migrations, since they are SQLite migrations and can't be used with PostgreSQL: `rm -r migrations/`.
3. Run `wasp start db` to start your new db running (or check instructions above if you prefer using your custom db). Leave it running, since we need it for the next step.
4. In a different terminal, run `wasp db migrate-dev` to apply new changes and create new, initial migration.
5. That is it, you are all done!