mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-24 01:22:24 +03:00
Bring Prisma entities to the frontend (#962)
* Support typing backend queries * Implement types for actions and extract entities * Add Prisma entities to the frontend * Add frontend types to internal todoApp * Undo moving for easier review * Revert back to generating a query.js file * Rename buildEntityData * Remove solved todo in schema template * Fix docs in method * Adding uninstall command (#953) * Adding uninstall command * Updates docs and changelog related to the uninstall command * Use StrongPath instead of FilePath * Fix review feedback * Move prisma client generation messages * Generalize FileDraft functions * Generate prisma clients using ENV * Fix schema checksum check * Small refactor in db generator * Run prisma generate from server root dir * Fix types for useAction * Fix type error for useAction * Fix schema generation in Dockerfile * Refactor passing env vars to prisma schema * Fix useAction types * Replace Prelude readFile with SP readFile * Add comment for prisma/client in web app * Replace Prelude writeFile with SP writeFile * Replace do and if with ifM * Rename readProjectTelemetryFile * Refactor readOrCreateUserSignatureFile * Remove redundant comment * Fix typo in variable name * Simulate unions with a type class * Further improve strongpath types * Generate prisma clients after migration * Change ModuleRootDir to ComponentRootDir * Remove solved todo * Improve naming * Remove redundant env variable * Improve formatting * Fix errors after merging * Change local function name Co-authored-by: Martin Šošić <Martinsos@users.noreply.github.com> * Rename local function again * Update changelog * Fix error type in useQuery * Rename Component to AppComponent * Refactor DbGenerator * Explain Abs paths in SP helpers * Update e2e tests * Fix formatting * Change signature for doesFileExist * Change signature for SP functions * Remove redundant do block * Fix formatting * Reorder functions * Rename module to appComponent in functions * Rename module to component in functions * Rename telemetry cache function * Fix formatting --------- Co-authored-by: Mihovil Ilakovac <mihovil@ilakovac.com> Co-authored-by: Martin Šošić <Martinsos@users.noreply.github.com>
This commit is contained in:
parent
e4e29eec0b
commit
c41f065ef4
@ -5,15 +5,44 @@
|
||||
### `wasp deploy` CLI command added
|
||||
We have made it much easier to deploy your Wasp apps via a new CLI command, `wasp deploy`. 🚀 This release adds support for Fly.io, but we hope to add more hosting providers soon!
|
||||
|
||||
### Import Wasp entity types on the backend
|
||||
You can now import and use the types of Wasp entities in your backend code:
|
||||
### Import Wasp entity types (on frontend and backend)
|
||||
You can now import and use the types of Wasp entities anywhere in your code.
|
||||
|
||||
Let's assume your Wasp file contains the following entity:
|
||||
```c
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
psl=}
|
||||
```
|
||||
Here's how a file on your backend can use it:
|
||||
```typescript
|
||||
import { Task } from '@wasp/entities/Task'
|
||||
|
||||
const getTasks = (args, context): Task[] => {
|
||||
const getTasks = (args, context) => {
|
||||
const tasks: Task[] = // ...
|
||||
// ...
|
||||
}
|
||||
```
|
||||
And here's how a frontend component can use it:
|
||||
|
||||
```typescript
|
||||
// ...
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getTasks from '@wasp/queries/getTasks.js'
|
||||
import { Task } from '@wasp/entities'
|
||||
|
||||
type TaskPayload = Pick<Task, "id">
|
||||
|
||||
const Todo = (props: any) => {
|
||||
// The variable 'task' will now have the type Task.
|
||||
const { data: task } = useQuery<TaskPayload, Task>(getTask, { id: taskId })
|
||||
// ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### TypeScript support for Queries and Actions
|
||||
|
@ -7,7 +7,7 @@ import Control.Monad.Except (throwError)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Data.List (intercalate)
|
||||
import Path.IO (copyDirRecur, doesDirExist)
|
||||
import StrongPath (Abs, Dir, Path, Path', System, fromAbsFile, parseAbsDir, reldir, relfile, (</>))
|
||||
import StrongPath (Abs, Dir, Path, Path', System, parseAbsDir, reldir, relfile, (</>))
|
||||
import StrongPath.Path (toPathAbsDir)
|
||||
import System.Directory (getCurrentDirectory)
|
||||
import qualified System.FilePath as FP
|
||||
@ -18,6 +18,7 @@ import Wasp.Common (WaspProjectDir)
|
||||
import qualified Wasp.Common as Common (WaspProjectDir)
|
||||
import qualified Wasp.Data as Data
|
||||
import Wasp.Util (indent, kebabToCamelCase)
|
||||
import qualified Wasp.Util.IO as IOUtil
|
||||
import qualified Wasp.Util.Terminal as Term
|
||||
import qualified Wasp.Version as WV
|
||||
|
||||
@ -85,9 +86,9 @@ initializeProjectFromSkeleton absWaspProjectDir = do
|
||||
copyDirRecur (toPathAbsDir absSkeletonDir) (toPathAbsDir absWaspProjectDir)
|
||||
|
||||
writeMainWaspFile :: Path System Abs (Dir WaspProjectDir) -> ProjectInfo -> IO ()
|
||||
writeMainWaspFile waspProjectDir (ProjectInfo projectName appName) = writeFile absMainWaspFile mainWaspFileContent
|
||||
writeMainWaspFile waspProjectDir (ProjectInfo projectName appName) = IOUtil.writeFile absMainWaspFile mainWaspFileContent
|
||||
where
|
||||
absMainWaspFile = fromAbsFile $ waspProjectDir </> [relfile|main.wasp|]
|
||||
absMainWaspFile = waspProjectDir </> [relfile|main.wasp|]
|
||||
mainWaspFileContent =
|
||||
unlines
|
||||
[ "app %s {" `printf` appName,
|
||||
|
@ -4,18 +4,18 @@ module Wasp.Cli.Command.Db.Migrate
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad.Except (throwError)
|
||||
import Control.Monad.Except (ExceptT (ExceptT), liftEither, runExceptT, throwError)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
-- Wasp generator interface.
|
||||
|
||||
import StrongPath ((</>))
|
||||
import StrongPath (Abs, Dir, Path', (</>))
|
||||
import Wasp.Cli.Command (Command, CommandError (..))
|
||||
import Wasp.Cli.Command.Common
|
||||
( findWaspProjectRootDirFromCwd,
|
||||
)
|
||||
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
|
||||
@ -26,26 +26,33 @@ import qualified Wasp.Message as Msg
|
||||
migrateDev :: [String] -> Command ()
|
||||
migrateDev optionalMigrateArgs = do
|
||||
waspProjectDir <- findWaspProjectRootDirFromCwd
|
||||
let genProjectRootDir =
|
||||
let waspDbMigrationsDir = waspProjectDir </> Wasp.Common.dbMigrationsDirInWaspProjectDir
|
||||
let projectRootDir =
|
||||
waspProjectDir
|
||||
</> Cli.Common.dotWaspDirInWaspProjectDir
|
||||
</> Cli.Common.generatedCodeDirInDotWaspDir
|
||||
|
||||
let waspDbMigrationsDir =
|
||||
waspProjectDir
|
||||
</> Wasp.Common.dbMigrationsDirInWaspProjectDir
|
||||
migrateDatabase optionalMigrateArgs projectRootDir waspDbMigrationsDir
|
||||
generatePrismaClients projectRootDir
|
||||
|
||||
let parsedMigrateArgs = parseMigrateArgs optionalMigrateArgs
|
||||
case parsedMigrateArgs of
|
||||
Left parseError ->
|
||||
throwError $ CommandError "Migrate dev failed" parseError
|
||||
Right migrateArgs -> do
|
||||
cliSendMessageC $ Msg.Start "Performing migration..."
|
||||
migrateResult <- liftIO $ DbOps.migrateDevAndCopyToSource waspDbMigrationsDir genProjectRootDir migrateArgs
|
||||
case migrateResult of
|
||||
Left migrateError ->
|
||||
throwError $ CommandError "Migrate dev failed" migrateError
|
||||
Right () -> cliSendMessageC $ Msg.Success "Migration done."
|
||||
migrateDatabase :: [String] -> Path' Abs (Dir ProjectRootDir) -> Path' Abs (Dir DbMigrationsDir) -> Command ()
|
||||
migrateDatabase optionalMigrateArgs projectRootDir dbMigrationsDir = do
|
||||
cliSendMessageC $ Msg.Start "Starting database migration..."
|
||||
liftIO tryMigrate >>= \case
|
||||
Left err -> throwError $ CommandError "Migrate dev failed" err
|
||||
Right () -> cliSendMessageC $ Msg.Success "Database successfully migrated."
|
||||
where
|
||||
tryMigrate = runExceptT $ 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).
|
||||
|
@ -7,9 +7,9 @@ where
|
||||
|
||||
import Control.Arrow
|
||||
import Control.Monad.Except
|
||||
import StrongPath (Abs, Dir, Path', fromAbsFile, fromRelFile, toFilePath)
|
||||
import StrongPath (Abs, Dir, Path', fromRelFile)
|
||||
import StrongPath.Operations
|
||||
import System.Directory (doesFileExist, getFileSize)
|
||||
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)
|
||||
@ -22,7 +22,8 @@ import Wasp.Common (WaspProjectDir)
|
||||
import Wasp.Error (showCompilerErrorForTerminal)
|
||||
import Wasp.Lib (findWaspFile)
|
||||
import qualified Wasp.Message as Msg
|
||||
import Wasp.Util.IO (listDirectoryDeep)
|
||||
import Wasp.Util (ifM)
|
||||
import qualified Wasp.Util.IO as IOUtil
|
||||
import qualified Wasp.Util.Terminal as Term
|
||||
|
||||
info :: Command ()
|
||||
@ -48,24 +49,26 @@ printInfo :: String -> String -> String
|
||||
printInfo key value = Term.applyStyles [Term.Cyan] key ++ ": " <> Term.applyStyles [Term.White] value
|
||||
|
||||
readDirectorySizeMB :: Path' Abs (Dir WaspProjectDir) -> IO String
|
||||
readDirectorySizeMB path = (++ " MB") . show . (`div` 1000000) . sum <$> (listDirectoryDeep path >>= mapM (getFileSize . fromRelFile))
|
||||
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 = do
|
||||
let dotWaspInfoFile =
|
||||
fromAbsFile $
|
||||
waspDir </> Cli.Common.dotWaspDirInWaspProjectDir
|
||||
</> Cli.Common.generatedCodeDirInDotWaspDir
|
||||
</> Cli.Common.dotWaspInfoFileInGeneratedCodeDir
|
||||
dotWaspInfoFileExists <- doesFileExist dotWaspInfoFile
|
||||
if dotWaspInfoFileExists
|
||||
then do readFile dotWaspInfoFile
|
||||
else return "No compile information found"
|
||||
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 $ readFile $ toFilePath waspFile
|
||||
waspStr <- liftIO $ IOUtil.readFile waspFile
|
||||
liftEither $ left (annotateErrorForCli waspFile waspStr) $ Analyzer.analyze waspStr
|
||||
where
|
||||
annotateErrorForCli waspFile waspStr =
|
||||
|
@ -40,7 +40,7 @@ telemetry = do
|
||||
|
||||
maybeProjectHash <- (Just <$> TlmProject.getWaspProjectPathHash) `catchError` const (return Nothing)
|
||||
for_ maybeProjectHash $ \projectHash -> do
|
||||
maybeProjectCache <- liftIO $ TlmProject.readProjectTelemetryFile telemetryCacheDirPath projectHash
|
||||
maybeProjectCache <- liftIO $ TlmProject.readProjectTelemetryCacheFile telemetryCacheDirPath projectHash
|
||||
for_ maybeProjectCache $ \projectCache -> do
|
||||
let maybeTimeOfLastSending = TlmProject.getTimeOfLastTelemetryDataSent projectCache
|
||||
for_ maybeTimeOfLastSending $ \timeOfLastSending -> do
|
||||
|
@ -3,7 +3,7 @@
|
||||
module Wasp.Cli.Command.Telemetry.Project
|
||||
( getWaspProjectPathHash,
|
||||
considerSendingData,
|
||||
readProjectTelemetryFile,
|
||||
readProjectTelemetryCacheFile,
|
||||
getTimeOfLastTelemetryDataSent,
|
||||
-- NOTE: for testing only
|
||||
checkIfEnvValueIsTruthy,
|
||||
@ -27,7 +27,6 @@ import qualified Network.HTTP.Simple as HTTP
|
||||
import Paths_waspc (version)
|
||||
import StrongPath (Abs, Dir, File', Path')
|
||||
import qualified StrongPath as SP
|
||||
import qualified System.Directory as SD
|
||||
import qualified System.Environment as ENV
|
||||
import qualified System.Info
|
||||
import Wasp.Cli.Command (Command)
|
||||
@ -35,6 +34,8 @@ import qualified Wasp.Cli.Command.Call as Command.Call
|
||||
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd)
|
||||
import Wasp.Cli.Command.Telemetry.Common (TelemetryCacheDir)
|
||||
import Wasp.Cli.Command.Telemetry.User (UserSignature (..))
|
||||
import Wasp.Util (ifM)
|
||||
import qualified Wasp.Util.IO as IOUtil
|
||||
|
||||
considerSendingData :: Path' Abs (Dir TelemetryCacheDir) -> UserSignature -> ProjectHash -> Command.Call.Call -> IO ()
|
||||
considerSendingData telemetryCacheDirPath userSignature projectHash cmdCall = do
|
||||
@ -125,26 +126,28 @@ initialCache = ProjectTelemetryCache {_lastCheckIn = Nothing, _lastCheckInBuild
|
||||
getTimeOfLastTelemetryDataSent :: ProjectTelemetryCache -> Maybe T.UTCTime
|
||||
getTimeOfLastTelemetryDataSent cache = maximum [_lastCheckIn cache, _lastCheckInBuild cache]
|
||||
|
||||
readProjectTelemetryFile :: Path' Abs (Dir TelemetryCacheDir) -> ProjectHash -> IO (Maybe ProjectTelemetryCache)
|
||||
readProjectTelemetryFile telemetryCacheDirPath projectHash = do
|
||||
fileExists <- SD.doesFileExist filePathFP
|
||||
if fileExists then readCacheFile else return Nothing
|
||||
readProjectTelemetryCacheFile :: Path' Abs (Dir TelemetryCacheDir) -> ProjectHash -> IO (Maybe ProjectTelemetryCache)
|
||||
readProjectTelemetryCacheFile telemetryCacheDirPath projectHash =
|
||||
ifM
|
||||
(IOUtil.doesFileExist projectTelemetryFile)
|
||||
parseProjectTelemetryFile
|
||||
(return Nothing)
|
||||
where
|
||||
filePathFP = SP.fromAbsFile $ getProjectTelemetryFilePath telemetryCacheDirPath projectHash
|
||||
readCacheFile = Aeson.decode . ByteStringLazyUTF8.fromString <$> readFile filePathFP
|
||||
projectTelemetryFile = getProjectTelemetryFilePath telemetryCacheDirPath projectHash
|
||||
parseProjectTelemetryFile = Aeson.decode . ByteStringLazyUTF8.fromString <$> IOUtil.readFile projectTelemetryFile
|
||||
|
||||
readOrCreateProjectTelemetryFile :: Path' Abs (Dir TelemetryCacheDir) -> ProjectHash -> IO ProjectTelemetryCache
|
||||
readOrCreateProjectTelemetryFile telemetryCacheDirPath projectHash = do
|
||||
maybeProjectTelemetryCache <- readProjectTelemetryFile telemetryCacheDirPath projectHash
|
||||
maybeProjectTelemetryCache <- readProjectTelemetryCacheFile telemetryCacheDirPath projectHash
|
||||
case maybeProjectTelemetryCache of
|
||||
Just cache -> return cache
|
||||
Nothing -> writeProjectTelemetryFile telemetryCacheDirPath projectHash initialCache >> return initialCache
|
||||
|
||||
writeProjectTelemetryFile :: Path' Abs (Dir TelemetryCacheDir) -> ProjectHash -> ProjectTelemetryCache -> IO ()
|
||||
writeProjectTelemetryFile telemetryCacheDirPath projectHash cache = do
|
||||
writeFile filePathFP (ByteStringLazyUTF8.toString $ Aeson.encode cache)
|
||||
IOUtil.writeFile projectTelemetryFile (ByteStringLazyUTF8.toString $ Aeson.encode cache)
|
||||
where
|
||||
filePathFP = SP.fromAbsFile $ getProjectTelemetryFilePath telemetryCacheDirPath projectHash
|
||||
projectTelemetryFile = getProjectTelemetryFilePath telemetryCacheDirPath projectHash
|
||||
|
||||
getProjectTelemetryFilePath :: Path' Abs (Dir TelemetryCacheDir) -> ProjectHash -> Path' Abs File'
|
||||
getProjectTelemetryFilePath telemetryCacheDir (ProjectHash projectHash) =
|
||||
|
@ -9,10 +9,10 @@ where
|
||||
import qualified Data.UUID.V4 as UUID
|
||||
import StrongPath (Abs, Dir, File', Path', relfile)
|
||||
import qualified StrongPath as SP
|
||||
import qualified System.Directory as SD
|
||||
import qualified System.Environment as ENV
|
||||
import Wasp.Cli.Command.Telemetry.Common (TelemetryCacheDir)
|
||||
import Wasp.Util (checksumFromString, hexToString, orIfNothingM)
|
||||
import Wasp.Util (checksumFromString, hexToString, ifM, orIfNothingM)
|
||||
import qualified Wasp.Util.IO as IOUtil
|
||||
|
||||
-- Random, non-identifyable UUID used to represent user in analytics.
|
||||
newtype UserSignature = UserSignature {_userSignatureValue :: String} deriving (Show)
|
||||
@ -22,17 +22,18 @@ obtainUserSignature telemetryCacheDirPath =
|
||||
getUserSignatureFromEnv `orIfNothingM` readOrCreateUserSignatureFile telemetryCacheDirPath
|
||||
|
||||
readOrCreateUserSignatureFile :: Path' Abs (Dir TelemetryCacheDir) -> IO UserSignature
|
||||
readOrCreateUserSignatureFile telemetryCacheDirPath = do
|
||||
let filePath = getUserSignatureFilePath telemetryCacheDirPath
|
||||
let filePathFP = SP.fromAbsFile filePath
|
||||
fileExists <- SD.doesFileExist filePathFP
|
||||
readOrCreateUserSignatureFile telemetryCacheDirPath =
|
||||
UserSignature
|
||||
<$> if fileExists
|
||||
then readFile filePathFP
|
||||
else do
|
||||
userSignature <- show <$> UUID.nextRandom
|
||||
writeFile filePathFP userSignature
|
||||
return userSignature
|
||||
<$> ifM
|
||||
(IOUtil.doesFileExist userSignatureFile)
|
||||
(IOUtil.readFile userSignatureFile)
|
||||
createUserSignatureFile
|
||||
where
|
||||
userSignatureFile = getUserSignatureFilePath telemetryCacheDirPath
|
||||
createUserSignatureFile = do
|
||||
userSignature <- show <$> UUID.nextRandom
|
||||
IOUtil.writeFile userSignatureFile userSignature
|
||||
return userSignature
|
||||
|
||||
getUserSignatureFilePath :: Path' Abs (Dir TelemetryCacheDir) -> Path' Abs File'
|
||||
getUserSignatureFilePath telemetryCacheDir = telemetryCacheDir SP.</> [relfile|signature|]
|
||||
|
@ -33,7 +33,7 @@ COPY server/package*.json ./server/
|
||||
RUN cd server && npm install
|
||||
{=# usingPrisma =}
|
||||
COPY db/schema.prisma ./db/
|
||||
RUN cd server && npx prisma generate --schema=../db/schema.prisma
|
||||
RUN cd server && {= serverPrismaClientOutputDirEnv =} npx prisma generate --schema='{= dbSchemaFileFromServerDir =}'
|
||||
{=/ usingPrisma =}
|
||||
|
||||
|
||||
|
@ -7,8 +7,7 @@ datasource db {
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
{=! TODO(matija): this shouldn't be hardcoded, generator should provide this path. =}
|
||||
output = "../server/node_modules/.prisma/client"
|
||||
output = {=& prismaClientOutputDir =}
|
||||
}
|
||||
|
||||
{=# modelSchemas =}
|
||||
|
@ -14,14 +14,14 @@ export type Action<Input, Output> = (args?: Input) => Promise<Output>;
|
||||
* action with extra options.
|
||||
*
|
||||
*/
|
||||
export type ActionOptions<ActionInput, CachedData> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, CachedData>[]
|
||||
export type ActionOptions<ActionInput> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, any>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A documented (public) way to define optimistic updates.
|
||||
*/
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData = unknown> = {
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData> = {
|
||||
getQuerySpecifier: GetQuerySpecifier<ActionInput, CachedData>
|
||||
updateQuery: UpdateQuery<ActionInput, CachedData>
|
||||
}
|
||||
@ -50,9 +50,9 @@ export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
* @param actionOptions An options object for enhancing/decorating the given Action.
|
||||
* @returns A decorated Action with added behavior but an unchanged API.
|
||||
*/
|
||||
export function useAction<Input = unknown, Output = unknown, CachedData = unknown>(
|
||||
export function useAction<Input = unknown, Output = unknown>(
|
||||
actionFn: Action<Input, Output>,
|
||||
actionOptions?: ActionOptions<Input, CachedData>
|
||||
actionOptions?: ActionOptions<Input>
|
||||
): typeof actionFn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
{{={= =}=}}
|
||||
import {
|
||||
{=# entities =}
|
||||
{= name =},
|
||||
{=/ entities =}
|
||||
} from '@prisma/client'
|
||||
|
||||
export type {
|
||||
{=# entities =}
|
||||
{= name =},
|
||||
{=/ entities =}
|
||||
} from '@prisma/client'
|
||||
|
||||
export type WaspEntity =
|
||||
{=# entities =}
|
||||
| {= name =}
|
||||
{=/ entities =}
|
||||
| never
|
@ -12,7 +12,7 @@ const config = {
|
||||
all: {
|
||||
env,
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
databaseUrl: process.env.DATABASE_URL,
|
||||
databaseUrl: process.env.{= databaseUrlEnvVar =},
|
||||
frontendUrl: undefined,
|
||||
{=# isAuthEnabled =}
|
||||
auth: {
|
||||
|
@ -39,6 +39,7 @@ waspBuild/.wasp/build/web-app/src/actions/core.js
|
||||
waspBuild/.wasp/build/web-app/src/actions/index.ts
|
||||
waspBuild/.wasp/build/web-app/src/api.js
|
||||
waspBuild/.wasp/build/web-app/src/config.js
|
||||
waspBuild/.wasp/build/web-app/src/entities/index.ts
|
||||
waspBuild/.wasp/build/web-app/src/ext-src/Main.css
|
||||
waspBuild/.wasp/build/web-app/src/ext-src/MainPage.jsx
|
||||
waspBuild/.wasp/build/web-app/src/ext-src/react-app-env.d.ts
|
||||
|
@ -18,7 +18,7 @@
|
||||
"file",
|
||||
"db/schema.prisma"
|
||||
],
|
||||
"16c90bcaec8038a1b8bff30b2db2f7876b40c0e2e1b088076491f86f14d172c5"
|
||||
"6f7b1b109e332bad9eb3cda4a2caf4963f4918c91b546c06fa42d8986c0b94a2"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -228,7 +228,7 @@
|
||||
"file",
|
||||
"web-app/package.json"
|
||||
],
|
||||
"68c241389188ef0e6ec2397c89914d62e5a8f458543d08bfefeff49655e87c43"
|
||||
"f4118eb025537133d213d3ff53f67be00f07606e822ae823b00cec61d0ef4dd8"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -270,7 +270,7 @@
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"d6710101a02f23b53972e72d374747047e4251c0177fd5c626c0cffd060cd4a2"
|
||||
"0f294c2f1d50a1473f6d332ef17944e7475c7d83a2180f6e2c9c9aecf25439f4"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -286,6 +286,13 @@
|
||||
],
|
||||
"a30d5ae7c1b317a7132cac93f4b5bffc3daf11f4f07b5e0d977d063810ffdd11"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/entities/index.ts"
|
||||
],
|
||||
"49702253f6beeca8661f60f41741b0d2120e62face9a1fb827d1147bf2ded9af"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
|
@ -6,6 +6,6 @@ datasource db {
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../server/node_modules/.prisma/client"
|
||||
output = env("PRISMA_CLIENT_OUTPUT_DIR")
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash","version":"^4.17.21"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"react-scripts","version":"5.0.1"}],"devDependencies":[{"name":"typescript","version":"^4.8.4"},{"name":"@types/react","version":"^17.0.39"},{"name":"@types/react-dom","version":"^17.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@tsconfig/create-react-app","version":"^1.0.3"}]}}
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash","version":"^4.17.21"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"react-scripts","version":"5.0.1"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"typescript","version":"^4.8.4"},{"name":"@types/react","version":"^17.0.39"},{"name":"@types/react-dom","version":"^17.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@tsconfig/create-react-app","version":"^1.0.3"}]}}
|
@ -12,6 +12,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "4.5.0",
|
||||
"@tanstack/react-query": "^4.13.0",
|
||||
"axios": "^0.27.2",
|
||||
"react": "^17.0.2",
|
||||
|
@ -14,14 +14,14 @@ export type Action<Input, Output> = (args?: Input) => Promise<Output>;
|
||||
* action with extra options.
|
||||
*
|
||||
*/
|
||||
export type ActionOptions<ActionInput, CachedData> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, CachedData>[]
|
||||
export type ActionOptions<ActionInput> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, any>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A documented (public) way to define optimistic updates.
|
||||
*/
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData = unknown> = {
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData> = {
|
||||
getQuerySpecifier: GetQuerySpecifier<ActionInput, CachedData>
|
||||
updateQuery: UpdateQuery<ActionInput, CachedData>
|
||||
}
|
||||
@ -50,9 +50,9 @@ export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
* @param actionOptions An options object for enhancing/decorating the given Action.
|
||||
* @returns A decorated Action with added behavior but an unchanged API.
|
||||
*/
|
||||
export function useAction<Input = unknown, Output = unknown, CachedData = unknown>(
|
||||
export function useAction<Input = unknown, Output = unknown>(
|
||||
actionFn: Action<Input, Output>,
|
||||
actionOptions?: ActionOptions<Input, CachedData>
|
||||
actionOptions?: ActionOptions<Input>
|
||||
): typeof actionFn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
8
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/entities/index.ts
generated
Normal file
8
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/entities/index.ts
generated
Normal file
@ -0,0 +1,8 @@
|
||||
import {
|
||||
} from '@prisma/client'
|
||||
|
||||
export type {
|
||||
} from '@prisma/client'
|
||||
|
||||
export type WaspEntity =
|
||||
| never
|
@ -39,6 +39,7 @@ waspCompile/.wasp/out/web-app/src/actions/core.js
|
||||
waspCompile/.wasp/out/web-app/src/actions/index.ts
|
||||
waspCompile/.wasp/out/web-app/src/api.js
|
||||
waspCompile/.wasp/out/web-app/src/config.js
|
||||
waspCompile/.wasp/out/web-app/src/entities/index.ts
|
||||
waspCompile/.wasp/out/web-app/src/ext-src/Main.css
|
||||
waspCompile/.wasp/out/web-app/src/ext-src/MainPage.jsx
|
||||
waspCompile/.wasp/out/web-app/src/ext-src/react-app-env.d.ts
|
||||
|
@ -18,7 +18,7 @@
|
||||
"file",
|
||||
"db/schema.prisma"
|
||||
],
|
||||
"2cd8e420a90505150d496273ceca091869b33ca4e8bf82c59a3ea678c852d63b"
|
||||
"93f3b154b04fce7819e24aeb1691cd1c78f731f41a2f9e0213d54ef783f2bc38"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -228,7 +228,7 @@
|
||||
"file",
|
||||
"web-app/package.json"
|
||||
],
|
||||
"56ead359017cdbb350a4e609af2fa75a0411642cfb1734a6e8d6e53991bd54b3"
|
||||
"7ad19b71dd90a24658c8302c8cc9c245c6be7f74a0601fba6bc02d756988dabf"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -270,7 +270,7 @@
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"d6710101a02f23b53972e72d374747047e4251c0177fd5c626c0cffd060cd4a2"
|
||||
"0f294c2f1d50a1473f6d332ef17944e7475c7d83a2180f6e2c9c9aecf25439f4"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -286,6 +286,13 @@
|
||||
],
|
||||
"a30d5ae7c1b317a7132cac93f4b5bffc3daf11f4f07b5e0d977d063810ffdd11"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/entities/index.ts"
|
||||
],
|
||||
"49702253f6beeca8661f60f41741b0d2120e62face9a1fb827d1147bf2ded9af"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
|
@ -6,6 +6,6 @@ datasource db {
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../server/node_modules/.prisma/client"
|
||||
output = env("PRISMA_CLIENT_OUTPUT_DIR")
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash","version":"^4.17.21"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"react-scripts","version":"5.0.1"}],"devDependencies":[{"name":"typescript","version":"^4.8.4"},{"name":"@types/react","version":"^17.0.39"},{"name":"@types/react-dom","version":"^17.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@tsconfig/create-react-app","version":"^1.0.3"}]}}
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash","version":"^4.17.21"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"react-scripts","version":"5.0.1"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"typescript","version":"^4.8.4"},{"name":"@types/react","version":"^17.0.39"},{"name":"@types/react-dom","version":"^17.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@tsconfig/create-react-app","version":"^1.0.3"}]}}
|
@ -12,6 +12,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "4.5.0",
|
||||
"@tanstack/react-query": "^4.13.0",
|
||||
"axios": "^0.27.2",
|
||||
"react": "^17.0.2",
|
||||
|
@ -14,14 +14,14 @@ export type Action<Input, Output> = (args?: Input) => Promise<Output>;
|
||||
* action with extra options.
|
||||
*
|
||||
*/
|
||||
export type ActionOptions<ActionInput, CachedData> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, CachedData>[]
|
||||
export type ActionOptions<ActionInput> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, any>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A documented (public) way to define optimistic updates.
|
||||
*/
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData = unknown> = {
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData> = {
|
||||
getQuerySpecifier: GetQuerySpecifier<ActionInput, CachedData>
|
||||
updateQuery: UpdateQuery<ActionInput, CachedData>
|
||||
}
|
||||
@ -50,9 +50,9 @@ export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
* @param actionOptions An options object for enhancing/decorating the given Action.
|
||||
* @returns A decorated Action with added behavior but an unchanged API.
|
||||
*/
|
||||
export function useAction<Input = unknown, Output = unknown, CachedData = unknown>(
|
||||
export function useAction<Input = unknown, Output = unknown>(
|
||||
actionFn: Action<Input, Output>,
|
||||
actionOptions?: ActionOptions<Input, CachedData>
|
||||
actionOptions?: ActionOptions<Input>
|
||||
): typeof actionFn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
import {
|
||||
} from '@prisma/client'
|
||||
|
||||
export type {
|
||||
} from '@prisma/client'
|
||||
|
||||
export type WaspEntity =
|
||||
| never
|
@ -41,6 +41,7 @@ waspJob/.wasp/out/web-app/src/actions/core.js
|
||||
waspJob/.wasp/out/web-app/src/actions/index.ts
|
||||
waspJob/.wasp/out/web-app/src/api.js
|
||||
waspJob/.wasp/out/web-app/src/config.js
|
||||
waspJob/.wasp/out/web-app/src/entities/index.ts
|
||||
waspJob/.wasp/out/web-app/src/ext-src/Main.css
|
||||
waspJob/.wasp/out/web-app/src/ext-src/MainPage.jsx
|
||||
waspJob/.wasp/out/web-app/src/ext-src/react-app-env.d.ts
|
||||
|
@ -18,7 +18,7 @@
|
||||
"file",
|
||||
"db/schema.prisma"
|
||||
],
|
||||
"16c90bcaec8038a1b8bff30b2db2f7876b40c0e2e1b088076491f86f14d172c5"
|
||||
"6f7b1b109e332bad9eb3cda4a2caf4963f4918c91b546c06fa42d8986c0b94a2"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -242,7 +242,7 @@
|
||||
"file",
|
||||
"web-app/package.json"
|
||||
],
|
||||
"855d82b845cc7b96898013f78c0f5eff9a8b9b03a0c39536be69f9dfbc551da5"
|
||||
"00fad53072f4d6b670e391d28819c5a74dcfe33d376ce8e585ba540ed3fe1cf1"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -284,7 +284,7 @@
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"d6710101a02f23b53972e72d374747047e4251c0177fd5c626c0cffd060cd4a2"
|
||||
"0f294c2f1d50a1473f6d332ef17944e7475c7d83a2180f6e2c9c9aecf25439f4"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -300,6 +300,13 @@
|
||||
],
|
||||
"a30d5ae7c1b317a7132cac93f4b5bffc3daf11f4f07b5e0d977d063810ffdd11"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/entities/index.ts"
|
||||
],
|
||||
"49702253f6beeca8661f60f41741b0d2120e62face9a1fb827d1147bf2ded9af"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
|
@ -6,6 +6,6 @@ datasource db {
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../server/node_modules/.prisma/client"
|
||||
output = env("PRISMA_CLIENT_OUTPUT_DIR")
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash","version":"^4.17.21"},{"name":"pg-boss","version":"^8.0.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"react-scripts","version":"5.0.1"}],"devDependencies":[{"name":"typescript","version":"^4.8.4"},{"name":"@types/react","version":"^17.0.39"},{"name":"@types/react-dom","version":"^17.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@tsconfig/create-react-app","version":"^1.0.3"}]}}
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash","version":"^4.17.21"},{"name":"pg-boss","version":"^8.0.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"react-scripts","version":"5.0.1"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"typescript","version":"^4.8.4"},{"name":"@types/react","version":"^17.0.39"},{"name":"@types/react-dom","version":"^17.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@tsconfig/create-react-app","version":"^1.0.3"}]}}
|
@ -12,6 +12,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "4.5.0",
|
||||
"@tanstack/react-query": "^4.13.0",
|
||||
"axios": "^0.27.2",
|
||||
"react": "^17.0.2",
|
||||
|
@ -14,14 +14,14 @@ export type Action<Input, Output> = (args?: Input) => Promise<Output>;
|
||||
* action with extra options.
|
||||
*
|
||||
*/
|
||||
export type ActionOptions<ActionInput, CachedData> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, CachedData>[]
|
||||
export type ActionOptions<ActionInput> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, any>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A documented (public) way to define optimistic updates.
|
||||
*/
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData = unknown> = {
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData> = {
|
||||
getQuerySpecifier: GetQuerySpecifier<ActionInput, CachedData>
|
||||
updateQuery: UpdateQuery<ActionInput, CachedData>
|
||||
}
|
||||
@ -50,9 +50,9 @@ export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
* @param actionOptions An options object for enhancing/decorating the given Action.
|
||||
* @returns A decorated Action with added behavior but an unchanged API.
|
||||
*/
|
||||
export function useAction<Input = unknown, Output = unknown, CachedData = unknown>(
|
||||
export function useAction<Input = unknown, Output = unknown>(
|
||||
actionFn: Action<Input, Output>,
|
||||
actionOptions?: ActionOptions<Input, CachedData>
|
||||
actionOptions?: ActionOptions<Input>
|
||||
): typeof actionFn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
8
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/entities/index.ts
generated
Normal file
8
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/entities/index.ts
generated
Normal file
@ -0,0 +1,8 @@
|
||||
import {
|
||||
} from '@prisma/client'
|
||||
|
||||
export type {
|
||||
} from '@prisma/client'
|
||||
|
||||
export type WaspEntity =
|
||||
| never
|
@ -44,6 +44,7 @@ waspMigrate/.wasp/out/web-app/src/actions/core.js
|
||||
waspMigrate/.wasp/out/web-app/src/actions/index.ts
|
||||
waspMigrate/.wasp/out/web-app/src/api.js
|
||||
waspMigrate/.wasp/out/web-app/src/config.js
|
||||
waspMigrate/.wasp/out/web-app/src/entities/index.ts
|
||||
waspMigrate/.wasp/out/web-app/src/ext-src/Main.css
|
||||
waspMigrate/.wasp/out/web-app/src/ext-src/MainPage.jsx
|
||||
waspMigrate/.wasp/out/web-app/src/ext-src/react-app-env.d.ts
|
||||
|
@ -11,14 +11,14 @@
|
||||
"file",
|
||||
"Dockerfile"
|
||||
],
|
||||
"fc947858a99b55ff5963560b5201f3b54e4f302768ba4ab30f3ebe0cab14fb0f"
|
||||
"e5332a9cfefb7af077ef2eba70e9deb90693997ae4cd2e0256bbe2a2346c465d"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"db/schema.prisma"
|
||||
],
|
||||
"04a6c677bba33a37c60132e09b60fec7679403fed77ad4cb7d7c306a600cef24"
|
||||
"8d017edd849a861ae086850270a9f817bb4b75d9ee9ac27c08b0e9c29a16f6fe"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -228,7 +228,7 @@
|
||||
"file",
|
||||
"web-app/package.json"
|
||||
],
|
||||
"15dac783a05b96e341b491a40368f4cfed4784967ca6a7980583b33d080986fa"
|
||||
"7c3bc417dd899317da57af925acae935ff6884250f45b4b434e5bd172d811874"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -270,7 +270,7 @@
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"d6710101a02f23b53972e72d374747047e4251c0177fd5c626c0cffd060cd4a2"
|
||||
"0f294c2f1d50a1473f6d332ef17944e7475c7d83a2180f6e2c9c9aecf25439f4"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -286,6 +286,13 @@
|
||||
],
|
||||
"a30d5ae7c1b317a7132cac93f4b5bffc3daf11f4f07b5e0d977d063810ffdd11"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/entities/index.ts"
|
||||
],
|
||||
"acc242b6e297244848b08ea7a3f65980b255e747fce76ce6f5990be24cde0417"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
|
@ -28,7 +28,7 @@ WORKDIR /app
|
||||
COPY server/package*.json ./server/
|
||||
RUN cd server && npm install
|
||||
COPY db/schema.prisma ./db/
|
||||
RUN cd server && npx prisma generate --schema=../db/schema.prisma
|
||||
RUN cd server && PRISMA_CLIENT_OUTPUT_DIR=../server/node_modules/.prisma/client/ npx prisma generate --schema='../db/schema.prisma'
|
||||
|
||||
|
||||
# TODO: Use pm2?
|
||||
|
@ -6,7 +6,7 @@ datasource db {
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../server/node_modules/.prisma/client"
|
||||
output = env("PRISMA_CLIENT_OUTPUT_DIR")
|
||||
}
|
||||
|
||||
model Task {
|
||||
|
@ -1 +1 @@
|
||||
04a6c677bba33a37c60132e09b60fec7679403fed77ad4cb7d7c306a600cef24
|
||||
8d017edd849a861ae086850270a9f817bb4b75d9ee9ac27c08b0e9c29a16f6fe
|
@ -1 +1 @@
|
||||
04a6c677bba33a37c60132e09b60fec7679403fed77ad4cb7d7c306a600cef24
|
||||
8d017edd849a861ae086850270a9f817bb4b75d9ee9ac27c08b0e9c29a16f6fe
|
@ -1 +1 @@
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash","version":"^4.17.21"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"react-scripts","version":"5.0.1"}],"devDependencies":[{"name":"typescript","version":"^4.8.4"},{"name":"@types/react","version":"^17.0.39"},{"name":"@types/react-dom","version":"^17.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@tsconfig/create-react-app","version":"^1.0.3"}]}}
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash","version":"^4.17.21"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"react-scripts","version":"5.0.1"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"typescript","version":"^4.8.4"},{"name":"@types/react","version":"^17.0.39"},{"name":"@types/react-dom","version":"^17.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@tsconfig/create-react-app","version":"^1.0.3"}]}}
|
@ -12,6 +12,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "4.5.0",
|
||||
"@tanstack/react-query": "^4.13.0",
|
||||
"axios": "^0.27.2",
|
||||
"react": "^17.0.2",
|
||||
|
@ -14,14 +14,14 @@ export type Action<Input, Output> = (args?: Input) => Promise<Output>;
|
||||
* action with extra options.
|
||||
*
|
||||
*/
|
||||
export type ActionOptions<ActionInput, CachedData> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, CachedData>[]
|
||||
export type ActionOptions<ActionInput> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, any>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A documented (public) way to define optimistic updates.
|
||||
*/
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData = unknown> = {
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData> = {
|
||||
getQuerySpecifier: GetQuerySpecifier<ActionInput, CachedData>
|
||||
updateQuery: UpdateQuery<ActionInput, CachedData>
|
||||
}
|
||||
@ -50,9 +50,9 @@ export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
* @param actionOptions An options object for enhancing/decorating the given Action.
|
||||
* @returns A decorated Action with added behavior but an unchanged API.
|
||||
*/
|
||||
export function useAction<Input = unknown, Output = unknown, CachedData = unknown>(
|
||||
export function useAction<Input = unknown, Output = unknown>(
|
||||
actionFn: Action<Input, Output>,
|
||||
actionOptions?: ActionOptions<Input, CachedData>
|
||||
actionOptions?: ActionOptions<Input>
|
||||
): typeof actionFn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
11
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/entities/index.ts
generated
Normal file
11
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/entities/index.ts
generated
Normal file
@ -0,0 +1,11 @@
|
||||
import {
|
||||
Task,
|
||||
} from '@prisma/client'
|
||||
|
||||
export type {
|
||||
Task,
|
||||
} from '@prisma/client'
|
||||
|
||||
export type WaspEntity =
|
||||
| Task
|
||||
| never
|
@ -2,20 +2,13 @@ import React, { useState, FormEventHandler, ChangeEventHandler } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { useAction } from '@wasp/actions'
|
||||
import { OptimisticUpdateDefinition, useAction } from '@wasp/actions'
|
||||
import getTasks from '@wasp/queries/getTasks.js'
|
||||
import createTask from '@wasp/actions/createTask.js'
|
||||
import updateTaskIsDone from '@wasp/actions/updateTaskIsDone.js'
|
||||
import deleteCompletedTasks from '@wasp/actions/deleteCompletedTasks.js'
|
||||
import toggleAllTasks from '@wasp/actions/toggleAllTasks.js'
|
||||
|
||||
// Copied from Prisma
|
||||
type Task = {
|
||||
id: number
|
||||
description: string
|
||||
isDone: boolean
|
||||
userId: number
|
||||
}
|
||||
import { Task } from '@wasp/entities'
|
||||
|
||||
type GetTasksError = { message: string }
|
||||
|
||||
@ -91,7 +84,7 @@ const Tasks = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
|
||||
<div>
|
||||
<table className='border-separate border-spacing-2'>
|
||||
<tbody>
|
||||
{tasks.map((task, idx) => <Task task={task} key={idx} />)}
|
||||
{tasks.map((task, idx) => <TaskView task={task} key={idx} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -100,8 +93,8 @@ const Tasks = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
|
||||
|
||||
type UpdateTaskIsDonePayload = Pick<Task, "id" | "isDone">
|
||||
|
||||
const Task = ({ task }: { task: Task }) => {
|
||||
const updateTaskIsDoneOptimistically = useAction<UpdateTaskIsDonePayload, void, Task[]>(updateTaskIsDone, {
|
||||
const TaskView = ({ task }: { task: Task }) => {
|
||||
const updateTaskIsDoneOptimistically = useAction(updateTaskIsDone, {
|
||||
optimisticUpdates: [{
|
||||
getQuerySpecifier: () => [getTasks],
|
||||
updateQuery: (updatedTask, oldTasks) => {
|
||||
@ -112,7 +105,7 @@ const Task = ({ task }: { task: Task }) => {
|
||||
return oldTasks.map(task => task.id === updatedTask.id ? { ...task, ...updatedTask } : task)
|
||||
}
|
||||
}
|
||||
}]
|
||||
} as OptimisticUpdateDefinition<UpdateTaskIsDonePayload, Task[]>]
|
||||
});
|
||||
const handleTaskIsDoneChange: ChangeEventHandler<HTMLInputElement> = async (event) => {
|
||||
const id = parseInt(event.target.id)
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { User } from '@wasp/entities'
|
||||
|
||||
export const ProfilePage = ({ user }) => {
|
||||
export const ProfilePage = ({ username }: User) => {
|
||||
return (
|
||||
<>
|
||||
<div>I am Profile page for { user.username }!</div>
|
||||
<div>I am Profile page for { username }!</div>
|
||||
<br />
|
||||
<Link to='/'>Go to dashboard</Link>
|
||||
</>
|
@ -2,44 +2,42 @@ import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { useAction } from '@wasp/actions'
|
||||
import { OptimisticUpdateDefinition, useAction } from '@wasp/actions'
|
||||
import updateTaskIsDone from '@wasp/actions/updateTaskIsDone'
|
||||
import getTask from '@wasp/queries/getTask.js'
|
||||
import getTasks from '@wasp/queries/getTasks.js'
|
||||
import { Task } from '@wasp/entities'
|
||||
|
||||
const Todo = (props) => {
|
||||
type TaskPayload = Pick<Task, "id" | "isDone">
|
||||
|
||||
const Todo = (props: any) => {
|
||||
const taskId = parseInt(props.match.params.id)
|
||||
const { data: task, isFetching, error } = useQuery(getTask, { id: taskId })
|
||||
|
||||
const { data: task, isFetching, error } = useQuery<Pick<Task, "id">, Task>(getTask, { id: taskId })
|
||||
|
||||
const updateTaskIsDoneOptimistically = useAction(updateTaskIsDone, {
|
||||
optimisticUpdates: [
|
||||
{
|
||||
getQuerySpecifier: () => [getTask, { id: taskId }],
|
||||
// This query's cache should should never be emtpy
|
||||
updateQuery: ({ isDone }, oldTask) => ({ ...oldTask, isDone }),
|
||||
},
|
||||
updateQuery: ({ isDone }, oldTask) => ({ ...oldTask!, isDone }),
|
||||
} as OptimisticUpdateDefinition<TaskPayload, Task>,
|
||||
{
|
||||
getQuerySpecifier: () => [getTasks],
|
||||
updateQuery: (updatedTask, oldTasks) => {
|
||||
if (oldTasks === undefined) {
|
||||
// cache is empty
|
||||
return [updatedTask]
|
||||
} else {
|
||||
return oldTasks.map(task =>
|
||||
task.id === updatedTask.id ? { ...task, ...updatedTask } : task
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
updateQuery: (updatedTask, oldTasks) =>
|
||||
oldTasks && oldTasks.map(task =>
|
||||
task.id === updatedTask.id ? { ...task, ...updatedTask } : task
|
||||
),
|
||||
} as OptimisticUpdateDefinition<TaskPayload, Task[]>
|
||||
]
|
||||
})
|
||||
|
||||
if (!task) return <div>Task with id {taskId} does not exist.</div>
|
||||
if (error) return <div>Error occurred! {error}</div>
|
||||
|
||||
async function toggleIsDone() {
|
||||
async function toggleIsDone({ id, isDone }: Task) {
|
||||
try {
|
||||
updateTaskIsDoneOptimistically({ id: task.id, isDone: !task.isDone })
|
||||
updateTaskIsDoneOptimistically({ id, isDone: !isDone })
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
@ -55,7 +53,7 @@ const Todo = (props) => {
|
||||
<div> id: {task.id} </div>
|
||||
<div> description: {task.description} </div>
|
||||
<div> is done: {task.isDone ? 'Yes' : 'No'} </div>
|
||||
<button onClick={toggleIsDone}>Mark as {task.isDone ? 'undone' : 'done'}</button>
|
||||
<button onClick={() => toggleIsDone(task)}>Mark as {task.isDone ? 'undone' : 'done'}</button>
|
||||
</>
|
||||
)}
|
||||
<br />
|
@ -1,7 +1,6 @@
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
import { Task } from '@wasp/entities/'
|
||||
import { AuthenticatedAction } from '@wasp/types'
|
||||
import { getSomeResource } from './serverSetup.js'
|
||||
import { Task } from '@wasp/entities'
|
||||
import {
|
||||
CreateTask,
|
||||
DeleteCompletedTasks,
|
||||
|
@ -1,17 +1,40 @@
|
||||
module Wasp.Generator.Common
|
||||
( ProjectRootDir,
|
||||
ServerRootDir,
|
||||
WebAppRootDir,
|
||||
AppComponentRootDir,
|
||||
DbRootDir,
|
||||
latestMajorNodeVersion,
|
||||
nodeVersionRange,
|
||||
npmVersionRange,
|
||||
prismaVersion,
|
||||
makeJsonWithEntityNameAndPrismaIdentifier,
|
||||
entityNameToPrismaIdentifier,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson (KeyValue ((.=)), object)
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Char (toLower)
|
||||
import qualified Wasp.SemanticVersion as SV
|
||||
|
||||
-- | Directory where the whole web app project (client, server, ...) is generated.
|
||||
data ProjectRootDir
|
||||
|
||||
class AppComponentRootDir d
|
||||
|
||||
data ServerRootDir
|
||||
|
||||
instance AppComponentRootDir ServerRootDir
|
||||
|
||||
data WebAppRootDir
|
||||
|
||||
instance AppComponentRootDir WebAppRootDir
|
||||
|
||||
data DbRootDir
|
||||
|
||||
instance AppComponentRootDir DbRootDir
|
||||
|
||||
-- | Latest concrete major node version supported by the nodeVersionRange, and
|
||||
-- therefore by Wasp.
|
||||
-- Here we assume that nodeVersionRange is using latestNodeLTSVersion as its basis.
|
||||
@ -34,3 +57,18 @@ npmVersionRange = SV.Range [SV.backwardsCompatibleWith latestLTSVersion]
|
||||
|
||||
prismaVersion :: SV.Version
|
||||
prismaVersion = SV.Version 4 5 0
|
||||
|
||||
makeJsonWithEntityNameAndPrismaIdentifier :: String -> Aeson.Value
|
||||
makeJsonWithEntityNameAndPrismaIdentifier name =
|
||||
object
|
||||
[ "name" .= name,
|
||||
"prismaIdentifier" .= entityNameToPrismaIdentifier name
|
||||
]
|
||||
|
||||
-- | Takes a Wasp Entity name (like `SomeTask` from `entity SomeTask {...}`) and
|
||||
-- converts it into a corresponding Prisma identifier (e.g., `someTask` used in
|
||||
-- `prisma.someTask`). This is what Prisma implicitly does when translating
|
||||
-- `model` declarations to client SDK identifiers. Useful when creating
|
||||
-- `context.entities` JS objects in Wasp templates.
|
||||
entityNameToPrismaIdentifier :: String -> String
|
||||
entityNameToPrismaIdentifier entityName = toLower (head entityName) : tail entityName
|
||||
|
@ -3,16 +3,14 @@
|
||||
module Wasp.Generator.DbGenerator
|
||||
( genDb,
|
||||
warnIfDbNeedsMigration,
|
||||
genPrismaClient,
|
||||
postWriteDbGeneratorActions,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson (object, (.=))
|
||||
import Data.Maybe (fromMaybe, maybeToList)
|
||||
import StrongPath (Abs, Dir, Path', (</>))
|
||||
import StrongPath (Abs, Dir, File, Path', Rel, (</>))
|
||||
import qualified StrongPath as SP
|
||||
import System.Directory (doesFileExist)
|
||||
import Wasp.AppSpec (AppSpec, getEntities)
|
||||
import qualified Wasp.AppSpec as AS
|
||||
import qualified Wasp.AppSpec.App as AS.App
|
||||
@ -21,13 +19,18 @@ import qualified Wasp.AppSpec.Entity as AS.Entity
|
||||
import Wasp.AppSpec.Valid (getApp)
|
||||
import Wasp.Generator.Common (ProjectRootDir)
|
||||
import Wasp.Generator.DbGenerator.Common
|
||||
( dbMigrationsDirInDbRootDir,
|
||||
( DbSchemaChecksumFile,
|
||||
DbSchemaChecksumOnLastDbConcurrenceFile,
|
||||
PrismaDbSchema,
|
||||
databaseUrlEnvVar,
|
||||
dbMigrationsDirInDbRootDir,
|
||||
dbRootDirInProjectRootDir,
|
||||
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir,
|
||||
dbSchemaChecksumOnLastGenerateFileProjectRootDir,
|
||||
dbSchemaFileInDbTemplatesDir,
|
||||
dbSchemaFileInProjectRootDir,
|
||||
dbTemplatesDirInTemplatesDir,
|
||||
prismaClientOutputDirEnvVar,
|
||||
)
|
||||
import qualified Wasp.Generator.DbGenerator.Operations as DbOps
|
||||
import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft)
|
||||
@ -40,15 +43,19 @@ import Wasp.Generator.Monad
|
||||
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, (<:>))
|
||||
import qualified Wasp.Util.IO as IOUtil
|
||||
|
||||
genDb :: AppSpec -> Generator [FileDraft]
|
||||
genDb spec =
|
||||
genPrismaSchema spec <:> (maybeToList <$> genMigrationsDir spec)
|
||||
genPrismaSchema spec
|
||||
<:> (maybeToList <$> genMigrationsDir spec)
|
||||
|
||||
genPrismaSchema :: AppSpec -> Generator FileDraft
|
||||
genPrismaSchema ::
|
||||
AppSpec ->
|
||||
Generator FileDraft
|
||||
genPrismaSchema spec = do
|
||||
(datasourceProvider, datasourceUrl) <- case dbSystem of
|
||||
AS.Db.PostgreSQL -> return ("postgresql", "env(\"DATABASE_URL\")")
|
||||
(datasourceProvider :: String, datasourceUrl) <- case dbSystem of
|
||||
AS.Db.PostgreSQL -> return ("postgresql", makeEnvVarField databaseUrlEnvVar)
|
||||
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 ."
|
||||
@ -57,15 +64,16 @@ genPrismaSchema spec = do
|
||||
let templateData =
|
||||
object
|
||||
[ "modelSchemas" .= map entityToPslModelSchema (AS.getDecls @AS.Entity.Entity spec),
|
||||
"datasourceProvider" .= (datasourceProvider :: String),
|
||||
"datasourceUrl" .= (datasourceUrl :: String)
|
||||
"datasourceProvider" .= datasourceProvider,
|
||||
"datasourceUrl" .= datasourceUrl,
|
||||
"prismaClientOutputDir" .= makeEnvVarField prismaClientOutputDirEnvVar
|
||||
]
|
||||
|
||||
return $ createTemplateFileDraft dstPath tmplSrcPath (Just templateData)
|
||||
return $ createTemplateFileDraft dbSchemaFileInProjectRootDir tmplSrcPath (Just templateData)
|
||||
where
|
||||
dstPath = dbSchemaFileInProjectRootDir
|
||||
tmplSrcPath = dbTemplatesDirInTemplatesDir </> dbSchemaFileInDbTemplatesDir
|
||||
dbSystem = fromMaybe AS.Db.SQLite (AS.Db.system =<< AS.App.db (snd $ getApp spec))
|
||||
dbSystem = fromMaybe AS.Db.SQLite $ AS.Db.system =<< AS.App.db (snd $ getApp spec)
|
||||
makeEnvVarField envVarName = "env(\"" ++ envVarName ++ "\")"
|
||||
|
||||
entityToPslModelSchema :: (String, AS.Entity.Entity) -> String
|
||||
entityToPslModelSchema (entityName, entity) =
|
||||
@ -84,7 +92,7 @@ genMigrationsDir spec =
|
||||
postWriteDbGeneratorActions :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ([GeneratorWarning], [GeneratorError])
|
||||
postWriteDbGeneratorActions spec dstDir = do
|
||||
dbGeneratorWarnings <- maybeToList <$> warnIfDbNeedsMigration spec dstDir
|
||||
dbGeneratorErrors <- maybeToList <$> genPrismaClient spec dstDir
|
||||
dbGeneratorErrors <- maybeToList <$> genPrismaClients spec dstDir
|
||||
return (dbGeneratorWarnings, dbGeneratorErrors)
|
||||
|
||||
-- | Checks if user needs to run `wasp db migrate-dev` due to changes in schema.prisma, and if so, returns a warning.
|
||||
@ -108,7 +116,7 @@ postWriteDbGeneratorActions spec dstDir = do
|
||||
-- in sync with the database and all migrations are applied, we generate that file to avoid future checks.
|
||||
warnIfDbNeedsMigration :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorWarning)
|
||||
warnIfDbNeedsMigration spec projectRootDir = do
|
||||
dbSchemaChecksumFileExists <- doesFileExist dbSchemaChecksumFp
|
||||
dbSchemaChecksumFileExists <- IOUtil.doesFileExist dbSchemaChecksumFp
|
||||
if dbSchemaChecksumFileExists
|
||||
then warnIfSchemaDiffersFromChecksum dbSchemaFp dbSchemaChecksumFp
|
||||
else
|
||||
@ -116,17 +124,19 @@ warnIfDbNeedsMigration spec projectRootDir = do
|
||||
then warnProjectDiffersFromDb projectRootDir
|
||||
else return Nothing
|
||||
where
|
||||
dbSchemaFp = SP.fromAbsFile $ projectRootDir </> dbSchemaFileInProjectRootDir
|
||||
dbSchemaChecksumFp = SP.fromAbsFile $ projectRootDir </> dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
|
||||
dbSchemaFp = projectRootDir </> dbSchemaFileInProjectRootDir
|
||||
dbSchemaChecksumFp = projectRootDir </> dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
|
||||
entitiesExist = not . null $ getEntities spec
|
||||
|
||||
warnIfSchemaDiffersFromChecksum :: FilePath -> FilePath -> IO (Maybe GeneratorWarning)
|
||||
warnIfSchemaDiffersFromChecksum dbSchemaFp dbSchemaChecksumFp = do
|
||||
dbSchemaFileChecksum <- hexToString <$> checksumFromFilePath dbSchemaFp
|
||||
dbChecksumFileContents <- readFile dbSchemaChecksumFp
|
||||
if dbSchemaFileChecksum /= dbChecksumFileContents
|
||||
then return . Just $ GeneratorNeedsMigrationWarning "Your Prisma schema has changed, please run `wasp db migrate-dev` when ready."
|
||||
else return Nothing
|
||||
warnIfSchemaDiffersFromChecksum ::
|
||||
Path' Abs (File PrismaDbSchema) ->
|
||||
Path' Abs (File 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.")
|
||||
|
||||
-- | 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.
|
||||
@ -140,35 +150,54 @@ 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 (SP.castFile dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir)
|
||||
DbOps.writeDbSchemaChecksumToFile projectRootDir 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 DATABASE_URL is wrong, or database is down, or any other number of causes.
|
||||
-- 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."
|
||||
|
||||
genPrismaClient :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorError)
|
||||
genPrismaClient spec projectRootDir = do
|
||||
ifM wasCurrentSchemaAlreadyGenerated (return Nothing) generatePrismaClientIfEntitiesExist
|
||||
genPrismaClients :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorError)
|
||||
genPrismaClients spec projectRootDir =
|
||||
ifM
|
||||
wasCurrentSchemaAlreadyGenerated
|
||||
(return Nothing)
|
||||
generatePrismaClientsIfEntitiesExist
|
||||
where
|
||||
wasCurrentSchemaAlreadyGenerated :: IO Bool
|
||||
wasCurrentSchemaAlreadyGenerated = do
|
||||
let dbSchemaFp = SP.fromAbsFile $ projectRootDir SP.</> dbSchemaFileInProjectRootDir
|
||||
let dbSchemaChecksumFp = SP.fromAbsFile $ projectRootDir SP.</> dbSchemaChecksumOnLastGenerateFileProjectRootDir
|
||||
wasCurrentSchemaAlreadyGenerated =
|
||||
checksumFileExistsAndMatchesSchema projectRootDir dbSchemaChecksumOnLastGenerateFileProjectRootDir
|
||||
|
||||
dbSchemaChecksumFileExists <- doesFileExist dbSchemaChecksumFp
|
||||
if dbSchemaChecksumFileExists
|
||||
then do
|
||||
dbSchemaFileChecksum <- hexToString <$> checksumFromFilePath dbSchemaFp
|
||||
dbChecksumFileContents <- readFile dbSchemaChecksumFp
|
||||
return $ dbSchemaFileChecksum == dbChecksumFileContents
|
||||
else return False
|
||||
generatePrismaClientsIfEntitiesExist :: IO (Maybe GeneratorError)
|
||||
generatePrismaClientsIfEntitiesExist
|
||||
| entitiesExist =
|
||||
either (Just . GenericGeneratorError) (const Nothing) <$> DbOps.generatePrismaClients projectRootDir
|
||||
| otherwise = return Nothing
|
||||
|
||||
generatePrismaClientIfEntitiesExist :: IO (Maybe GeneratorError)
|
||||
generatePrismaClientIfEntitiesExist = do
|
||||
let entitiesExist = not . null $ getEntities spec
|
||||
if entitiesExist
|
||||
then either (Just . GenericGeneratorError) (const Nothing) <$> DbOps.generatePrismaClient projectRootDir
|
||||
else return Nothing
|
||||
entitiesExist = not . null $ getEntities spec
|
||||
|
||||
checksumFileExistsAndMatchesSchema ::
|
||||
DbSchemaChecksumFile f =>
|
||||
Path' Abs (Dir ProjectRootDir) ->
|
||||
Path' (Rel ProjectRootDir) (File f) ->
|
||||
IO Bool
|
||||
checksumFileExistsAndMatchesSchema projectRootDir dbSchemaChecksumInProjectDir =
|
||||
ifM
|
||||
(IOUtil.doesFileExist checksumFileAbs)
|
||||
(checksumFileMatchesSchema dbSchemaFileAbs checksumFileAbs)
|
||||
(return False)
|
||||
where
|
||||
dbSchemaFileAbs = projectRootDir </> dbSchemaFileInProjectRootDir
|
||||
checksumFileAbs = projectRootDir </> dbSchemaChecksumInProjectDir
|
||||
|
||||
checksumFileMatchesSchema :: DbSchemaChecksumFile f => Path' Abs (File PrismaDbSchema) -> Path' Abs (File f) -> IO Bool
|
||||
checksumFileMatchesSchema dbSchemaFileAbs dbSchemaChecksumFileAbs = do
|
||||
dbChecksumFileContents <- IOUtil.readFile dbSchemaChecksumFileAbs
|
||||
schemaFileHasChecksum dbSchemaFileAbs dbChecksumFileContents
|
||||
where
|
||||
schemaFileHasChecksum :: Path' Abs (File PrismaDbSchema) -> String -> IO Bool
|
||||
schemaFileHasChecksum schemaFile checksum = do
|
||||
dbSchemaFileChecksum <- hexToString <$> checksumFromFilePath schemaFile
|
||||
return $ dbSchemaFileChecksum == checksum
|
||||
|
@ -1,38 +1,63 @@
|
||||
module Wasp.Generator.DbGenerator.Common
|
||||
( dbMigrationsDirInDbRootDir,
|
||||
serverPrismaClientOutputDirEnv,
|
||||
webAppPrismaClientOutputDirEnv,
|
||||
prismaClientOutputDirInAppComponentDir,
|
||||
dbSchemaFileFromAppComponentDir,
|
||||
dbRootDirInProjectRootDir,
|
||||
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir,
|
||||
dbSchemaChecksumOnLastGenerateFileProjectRootDir,
|
||||
dbSchemaFileInDbTemplatesDir,
|
||||
dbSchemaFileInProjectRootDir,
|
||||
dbTemplatesDirInTemplatesDir,
|
||||
defaultMigrateArgs,
|
||||
getOnLastDbConcurrenceChecksumFileRefreshAction,
|
||||
MigrateArgs (..),
|
||||
RefreshOnLastDbConcurrenceChecksumFile (..),
|
||||
DbSchemaChecksumOnLastDbConcurrenceFile,
|
||||
DbSchemaChecksumOnLastGenerateFile,
|
||||
PrismaDbSchema,
|
||||
serverRootDirFromDbRootDir,
|
||||
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 (ProjectRootDir)
|
||||
import Wasp.Generator.Common (AppComponentRootDir, DbRootDir, ProjectRootDir, ServerRootDir)
|
||||
import Wasp.Generator.Templates (TemplatesDir)
|
||||
|
||||
data DbRootDir
|
||||
|
||||
data DbTemplatesDir
|
||||
|
||||
-- | This file represents the checksum of schema.prisma at the point
|
||||
-- | This file represents the Prisma db schema.
|
||||
data PrismaDbSchema
|
||||
|
||||
class DbSchemaChecksumFile f
|
||||
|
||||
-- | This file represents the checksum of the Prisma db schema at the point
|
||||
-- at which we last interacted with the DB to ensure they matched.
|
||||
-- It is used to help warn the user of instances when they may need to migrate.
|
||||
data DbSchemaChecksumOnLastDbConcurrenceFile
|
||||
|
||||
-- | This file represents the checksum of schema.prisma
|
||||
instance DbSchemaChecksumFile DbSchemaChecksumOnLastDbConcurrenceFile
|
||||
|
||||
-- | This file represents the checksum of the Prisma db schema
|
||||
-- at the point at which `prisma generate` was last run. It is used
|
||||
-- to know if we need to regenerate schema.prisma during web app generation or not.
|
||||
data DbSchemaChecksumOnLastGenerateFile
|
||||
|
||||
instance DbSchemaChecksumFile DbSchemaChecksumOnLastGenerateFile
|
||||
|
||||
serverRootDirFromDbRootDir :: Path' (Rel DbRootDir) (Dir ServerRootDir)
|
||||
serverRootDirFromDbRootDir = [reldir|../server|]
|
||||
|
||||
webAppRootDirFromDbRootDir :: Path' (Rel DbRootDir) (Dir ServerRootDir)
|
||||
webAppRootDirFromDbRootDir = [reldir|../web-app|]
|
||||
|
||||
dbRootDirInProjectRootDir :: Path' (Rel ProjectRootDir) (Dir DbRootDir)
|
||||
dbRootDirInProjectRootDir = [reldir|db|]
|
||||
|
||||
@ -42,12 +67,16 @@ dbTemplatesDirInTemplatesDir = [reldir|db|]
|
||||
dbSchemaFileInDbTemplatesDir :: Path' (Rel DbTemplatesDir) File'
|
||||
dbSchemaFileInDbTemplatesDir = [relfile|schema.prisma|]
|
||||
|
||||
dbSchemaFileInDbRootDir :: Path' (Rel DbRootDir) File'
|
||||
-- Generated schema file will be in the same relative location as the
|
||||
-- template file within templates dir.
|
||||
dbSchemaFileInDbRootDir = SP.castRel dbSchemaFileInDbTemplatesDir
|
||||
dbSchemaFileInDbRootDir :: Path' (Rel DbRootDir) (File PrismaDbSchema)
|
||||
dbSchemaFileInDbRootDir = [relfile|schema.prisma|]
|
||||
|
||||
dbSchemaFileInProjectRootDir :: Path' (Rel ProjectRootDir) File'
|
||||
dbRootDirFromAppComponentDir :: AppComponentRootDir d => Path' (Rel d) (Dir DbRootDir)
|
||||
dbRootDirFromAppComponentDir = [reldir|../db|]
|
||||
|
||||
dbSchemaFileFromAppComponentDir :: AppComponentRootDir d => Path' (Rel d) (File PrismaDbSchema)
|
||||
dbSchemaFileFromAppComponentDir = dbRootDirFromAppComponentDir </> dbSchemaFileInDbRootDir
|
||||
|
||||
dbSchemaFileInProjectRootDir :: Path' (Rel ProjectRootDir) (File PrismaDbSchema)
|
||||
dbSchemaFileInProjectRootDir = dbRootDirInProjectRootDir </> dbSchemaFileInDbRootDir
|
||||
|
||||
dbMigrationsDirInDbRootDir :: Path' (Rel DbRootDir) (Dir DbMigrationsDir)
|
||||
@ -65,6 +94,25 @@ dbSchemaChecksumOnLastGenerateFileInDbRootDir = [relfile|schema.prisma.wasp-gene
|
||||
dbSchemaChecksumOnLastGenerateFileProjectRootDir :: Path' (Rel ProjectRootDir) (File DbSchemaChecksumOnLastGenerateFile)
|
||||
dbSchemaChecksumOnLastGenerateFileProjectRootDir = dbRootDirInProjectRootDir </> dbSchemaChecksumOnLastGenerateFileInDbRootDir
|
||||
|
||||
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|]
|
||||
|
||||
serverPrismaClientOutputDirEnv :: (String, String)
|
||||
serverPrismaClientOutputDirEnv = appComponentPrismaClientOutputDirEnv serverRootDirFromDbRootDir
|
||||
|
||||
webAppPrismaClientOutputDirEnv :: (String, String)
|
||||
webAppPrismaClientOutputDirEnv = appComponentPrismaClientOutputDirEnv webAppRootDirFromDbRootDir
|
||||
|
||||
appComponentPrismaClientOutputDirEnv :: AppComponentRootDir d => Path' (Rel DbRootDir) (Dir d) -> (String, String)
|
||||
appComponentPrismaClientOutputDirEnv appComponentDirFromDbRootDir =
|
||||
(prismaClientOutputDirEnvVar, SP.fromRelDir $ appComponentDirFromDbRootDir </> prismaClientOutputDirInAppComponentDir)
|
||||
|
||||
data MigrateArgs = MigrateArgs
|
||||
{ _migrationName :: Maybe String,
|
||||
_isCreateOnlyMigration :: Bool
|
||||
|
@ -8,24 +8,20 @@ module Wasp.Generator.DbGenerator.Jobs
|
||||
)
|
||||
where
|
||||
|
||||
import StrongPath (Abs, Dir, File', Path', Rel, (</>))
|
||||
import StrongPath (Abs, Dir, 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 (..), dbSchemaFileInProjectRootDir)
|
||||
import Wasp.Generator.DbGenerator.Common
|
||||
( MigrateArgs (..),
|
||||
dbSchemaFileInProjectRootDir,
|
||||
)
|
||||
import Wasp.Generator.Job (JobType)
|
||||
import qualified Wasp.Generator.Job as J
|
||||
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
|
||||
import Wasp.Generator.Job.Process (runNodeCommandAsJob, runNodeCommandAsJobWithExtraEnv)
|
||||
import Wasp.Generator.ServerGenerator.Common (serverRootDirInProjectRootDir)
|
||||
|
||||
-- | NOTE: The expectation is that `npm install` was already executed
|
||||
-- such that we can use the locally installed package.
|
||||
-- This assumption is ok since it happens during compilation now.
|
||||
prismaInServerNodeModules :: Path' (Rel ProjectRootDir) File'
|
||||
prismaInServerNodeModules = serverRootDirInProjectRootDir </> [SP.relfile|./node_modules/.bin/prisma|]
|
||||
|
||||
absPrismaExecutableFp :: Path' Abs (Dir ProjectRootDir) -> FilePath
|
||||
absPrismaExecutableFp projectDir = SP.toFilePath $ projectDir </> prismaInServerNodeModules
|
||||
|
||||
migrateDev :: Path' Abs (Dir ProjectRootDir) -> MigrateArgs -> J.Job
|
||||
migrateDev projectDir migrateArgs = do
|
||||
let serverDir = projectDir </> serverRootDirInProjectRootDir
|
||||
@ -40,7 +36,15 @@ migrateDev projectDir migrateArgs = do
|
||||
-- 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] ++ asPrismaCliArgs migrateArgs
|
||||
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.
|
||||
@ -96,17 +100,28 @@ migrateStatus projectDir = do
|
||||
|
||||
-- | Runs `prisma studio` - Prisma's db inspector.
|
||||
runStudio :: Path' Abs (Dir ProjectRootDir) -> J.Job
|
||||
runStudio projectDir = do
|
||||
let serverDir = projectDir </> serverRootDirInProjectRootDir
|
||||
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir
|
||||
let prismaStudioCmdArgs = ["studio", "--schema", SP.toFilePath schemaFile]
|
||||
|
||||
runStudio projectDir =
|
||||
runNodeCommandAsJob serverDir (absPrismaExecutableFp projectDir) prismaStudioCmdArgs J.Db
|
||||
where
|
||||
serverDir = projectDir </> serverRootDirInProjectRootDir
|
||||
schemaFile = projectDir </> dbSchemaFileInProjectRootDir
|
||||
prismaStudioCmdArgs = ["studio", "--schema", SP.toFilePath schemaFile]
|
||||
|
||||
generatePrismaClient :: Path' Abs (Dir ProjectRootDir) -> J.Job
|
||||
generatePrismaClient projectDir = do
|
||||
let serverDir = projectDir </> serverRootDirInProjectRootDir
|
||||
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir
|
||||
let prismaGenerateCmdArgs = ["generate", "--schema", SP.toFilePath schemaFile]
|
||||
generatePrismaClient :: Path' Abs (Dir ProjectRootDir) -> (String, String) -> JobType -> J.Job
|
||||
generatePrismaClient projectDir prismaClientOutputDirEnv jobType =
|
||||
runNodeCommandAsJobWithExtraEnv envVars serverRootDir prismaExecutable prismaGenerateCmdArgs jobType
|
||||
where
|
||||
envVars = [prismaClientOutputDirEnv]
|
||||
serverRootDir = projectDir </> serverRootDirInProjectRootDir
|
||||
prismaExecutable = absPrismaExecutableFp projectDir
|
||||
prismaGenerateCmdArgs = ["generate", "--schema", schemaFile]
|
||||
schemaFile = SP.fromAbsFile $ projectDir </> dbSchemaFileInProjectRootDir
|
||||
|
||||
runNodeCommandAsJob serverDir (absPrismaExecutableFp projectDir) prismaGenerateCmdArgs J.Db
|
||||
-- | NOTE: The expectation is that `npm install` was already executed
|
||||
-- such that we can use the locally installed package.
|
||||
-- This assumption is ok since it happens during compilation now.
|
||||
absPrismaExecutableFp :: Path' Abs (Dir ProjectRootDir) -> FilePath
|
||||
absPrismaExecutableFp projectDir = SP.fromAbsFile prismaExecutableAbs
|
||||
where
|
||||
prismaExecutableAbs :: Path' Abs File'
|
||||
prismaExecutableAbs = projectDir </> serverRootDirInProjectRootDir </> [relfile|./node_modules/.bin/prisma|]
|
||||
|
@ -1,26 +1,28 @@
|
||||
module Wasp.Generator.DbGenerator.Operations
|
||||
( migrateDevAndCopyToSource,
|
||||
generatePrismaClient,
|
||||
generatePrismaClients,
|
||||
doesSchemaMatchDb,
|
||||
writeDbSchemaChecksumToFile,
|
||||
removeDbSchemaChecksumFile,
|
||||
areAllMigrationsAppliedToDb,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (liftA2)
|
||||
import Control.Concurrent (newChan)
|
||||
import Control.Concurrent.Async (concurrently)
|
||||
import Control.Monad (when)
|
||||
import Control.Monad.Catch (catch)
|
||||
import Control.Monad.Extra (whenM)
|
||||
import Data.Either (isRight)
|
||||
import qualified Path as P
|
||||
import StrongPath (Abs, Dir, File', Path', Rel)
|
||||
import StrongPath (Abs, Dir, File, Path', Rel, (</>))
|
||||
import qualified StrongPath as SP
|
||||
import System.Directory (doesFileExist, removeFile)
|
||||
import System.Exit (ExitCode (..))
|
||||
import Wasp.Common (DbMigrationsDir)
|
||||
import Wasp.Generator.Common (ProjectRootDir)
|
||||
import Wasp.Generator.DbGenerator.Common
|
||||
( MigrateArgs,
|
||||
( DbSchemaChecksumFile,
|
||||
MigrateArgs,
|
||||
RefreshOnLastDbConcurrenceChecksumFile (..),
|
||||
dbMigrationsDirInDbRootDir,
|
||||
dbRootDirInProjectRootDir,
|
||||
@ -28,12 +30,17 @@ import Wasp.Generator.DbGenerator.Common
|
||||
dbSchemaChecksumOnLastGenerateFileProjectRootDir,
|
||||
dbSchemaFileInProjectRootDir,
|
||||
getOnLastDbConcurrenceChecksumFileRefreshAction,
|
||||
serverPrismaClientOutputDirEnv,
|
||||
webAppPrismaClientOutputDirEnv,
|
||||
)
|
||||
import qualified Wasp.Generator.DbGenerator.Jobs as DbJobs
|
||||
import Wasp.Generator.FileDraft.WriteableMonad (WriteableMonad (copyDirectoryRecursive))
|
||||
import qualified Wasp.Generator.Job as J
|
||||
import Wasp.Generator.Job.IO (printJobMsgsUntilExitReceived, readJobMessagesAndPrintThemPrefixed)
|
||||
import qualified Wasp.Generator.WriteFileDrafts as Generator.WriteFileDrafts
|
||||
import Wasp.Util (checksumFromFilePath, hexToString)
|
||||
import Wasp.Util.IO (doesFileExist, removeFile)
|
||||
import qualified Wasp.Util.IO as IOUtil
|
||||
|
||||
-- | Migrates in the generated project context and then copies the migrations dir back
|
||||
-- up to the wasp project dir to ensure they remain in sync.
|
||||
@ -56,54 +63,73 @@ finalizeMigration genProjectRootDirAbs dbMigrationsDirInWaspProjectDirAbs onLast
|
||||
applyOnLastDbConcurrenceChecksumFileRefreshAction
|
||||
return res
|
||||
where
|
||||
dbMigrationsDirInProjectRootDir = dbRootDirInProjectRootDir SP.</> dbMigrationsDirInDbRootDir
|
||||
dbMigrationsDirInProjectRootDir = dbRootDirInProjectRootDir </> dbMigrationsDirInDbRootDir
|
||||
applyOnLastDbConcurrenceChecksumFileRefreshAction =
|
||||
case onLastDbConcurrenceChecksumFileRefreshAction of
|
||||
WriteOnLastDbConcurrenceChecksumFile ->
|
||||
writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir)
|
||||
writeDbSchemaChecksumToFile genProjectRootDirAbs dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
|
||||
RemoveOnLastDbConcurrenceChecksumFile ->
|
||||
removeDbSchemaChecksumFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir)
|
||||
removeDbSchemaChecksumFile genProjectRootDirAbs dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
|
||||
IgnoreOnLastDbConcurrenceChecksumFile -> return ()
|
||||
|
||||
-- | Copies the DB migrations from the generated project dir back up to theh wasp project dir
|
||||
copyMigrationsBackToSource :: Path' Abs (Dir ProjectRootDir) -> Path' Abs (Dir DbMigrationsDir) -> IO (Either String ())
|
||||
copyMigrationsBackToSource genProjectRootDirAbs dbMigrationsDirInWaspProjectDirAbs =
|
||||
do
|
||||
copyDirectoryRecursive genProjectMigrationsDir waspMigrationsDir >> return (Right ())
|
||||
`catch` (\e -> return $ Left $ show (e :: P.PathException))
|
||||
`catch` (\e -> return $ Left $ show (e :: IOError))
|
||||
copyDirectoryRecursive genProjectMigrationsDir waspMigrationsDir >> return (Right ())
|
||||
`catch` (\e -> return $ Left $ show (e :: P.PathException))
|
||||
`catch` (\e -> return $ Left $ show (e :: IOError))
|
||||
where
|
||||
waspMigrationsDir = SP.castDir dbMigrationsDirInWaspProjectDirAbs
|
||||
genProjectMigrationsDir = SP.castDir $ genProjectRootDirAbs SP.</> dbRootDirInProjectRootDir SP.</> dbMigrationsDirInDbRootDir
|
||||
waspMigrationsDir = dbMigrationsDirInWaspProjectDirAbs
|
||||
genProjectMigrationsDir = genProjectRootDirAbs </> dbRootDirInProjectRootDir </> dbMigrationsDirInDbRootDir
|
||||
|
||||
-- | This function assumes the DB schema has been generated, as it will attempt to read it from the generated code.
|
||||
writeDbSchemaChecksumToFile :: Path' Abs (Dir ProjectRootDir) -> Path' (Rel ProjectRootDir) File' -> IO ()
|
||||
writeDbSchemaChecksumToFile ::
|
||||
DbSchemaChecksumFile f =>
|
||||
Path' Abs (Dir ProjectRootDir) ->
|
||||
Path' (Rel ProjectRootDir) (File f) ->
|
||||
IO ()
|
||||
writeDbSchemaChecksumToFile genProjectRootDirAbs dbSchemaChecksumInProjectRootDir = do
|
||||
dbSchemaExists <- doesFileExist dbSchemaFp
|
||||
when dbSchemaExists $ do
|
||||
checksum <- hexToString <$> checksumFromFilePath dbSchemaFp
|
||||
writeFile dbSchemaChecksumFp checksum
|
||||
whenM (doesFileExist dbSchemaFile) $ do
|
||||
checksum <- hexToString <$> checksumFromFilePath dbSchemaFile
|
||||
IOUtil.writeFile dbSchemaChecksumFile checksum
|
||||
where
|
||||
dbSchemaFp = SP.fromAbsFile $ genProjectRootDirAbs SP.</> dbSchemaFileInProjectRootDir
|
||||
dbSchemaChecksumFp = SP.fromAbsFile $ genProjectRootDirAbs SP.</> dbSchemaChecksumInProjectRootDir
|
||||
dbSchemaFile = genProjectRootDirAbs </> dbSchemaFileInProjectRootDir
|
||||
dbSchemaChecksumFile = genProjectRootDirAbs </> dbSchemaChecksumInProjectRootDir
|
||||
|
||||
removeDbSchemaChecksumFile :: Path' Abs (Dir ProjectRootDir) -> Path' (Rel ProjectRootDir) File' -> IO ()
|
||||
removeDbSchemaChecksumFile genProjectRootDirAbs dbSchemaChecksumInProjectRootDir =
|
||||
let dbSchemaChecksumFp = SP.fromAbsFile $ genProjectRootDirAbs SP.</> dbSchemaChecksumInProjectRootDir
|
||||
in removeFile dbSchemaChecksumFp
|
||||
removeDbSchemaChecksumFile ::
|
||||
DbSchemaChecksumFile f =>
|
||||
Path' Abs (Dir ProjectRootDir) ->
|
||||
Path' (Rel ProjectRootDir) (File f) ->
|
||||
IO ()
|
||||
removeDbSchemaChecksumFile genProjectRootDirAbs dbSchemaChecksumInProjectRootDir = removeFile dbSchemaChecksumFp
|
||||
where
|
||||
dbSchemaChecksumFp = genProjectRootDirAbs </> dbSchemaChecksumInProjectRootDir
|
||||
|
||||
generatePrismaClient :: Path' Abs (Dir ProjectRootDir) -> IO (Either String ())
|
||||
generatePrismaClient genProjectRootDirAbs = do
|
||||
generatePrismaClients :: Path' Abs (Dir ProjectRootDir) -> IO (Either String ())
|
||||
generatePrismaClients projectRootDir = do
|
||||
generateResult <- liftA2 (>>) generatePrismaClientForServer generatePrismaClientForWebApp projectRootDir
|
||||
when (isRight generateResult) updateDbSchemaChecksumOnLastGenerate
|
||||
return generateResult
|
||||
where
|
||||
generatePrismaClientForServer = generatePrismaClient serverPrismaClientOutputDirEnv J.Server
|
||||
generatePrismaClientForWebApp = generatePrismaClient webAppPrismaClientOutputDirEnv J.WebApp
|
||||
updateDbSchemaChecksumOnLastGenerate =
|
||||
writeDbSchemaChecksumToFile projectRootDir dbSchemaChecksumOnLastGenerateFileProjectRootDir
|
||||
|
||||
generatePrismaClient ::
|
||||
(String, String) ->
|
||||
J.JobType ->
|
||||
Path' Abs (Dir ProjectRootDir) ->
|
||||
IO (Either String ())
|
||||
generatePrismaClient prismaClientOutputDirEnv jobType projectRootDir = do
|
||||
chan <- newChan
|
||||
(_, dbExitCode) <-
|
||||
(_, exitCode) <-
|
||||
concurrently
|
||||
(readJobMessagesAndPrintThemPrefixed chan)
|
||||
(DbJobs.generatePrismaClient genProjectRootDirAbs chan)
|
||||
case dbExitCode of
|
||||
ExitSuccess -> do
|
||||
writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastGenerateFileProjectRootDir)
|
||||
return $ Right ()
|
||||
ExitFailure code -> return $ Left $ "Prisma client generation failed with exit code: " ++ show code
|
||||
(DbJobs.generatePrismaClient projectRootDir prismaClientOutputDirEnv jobType chan)
|
||||
return $ case exitCode of
|
||||
ExitSuccess -> Right ()
|
||||
ExitFailure code -> Left $ "Prisma client generation failed with exit code: " ++ show code
|
||||
|
||||
-- | Checks `prisma migrate diff` exit code to determine if schema.prisma is
|
||||
-- different than the DB. Returns Nothing on error as we do not know the current state.
|
||||
|
@ -11,17 +11,28 @@ import Data.Aeson (object, (.=))
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import StrongPath (File', Path', Rel, relfile)
|
||||
import StrongPath (File, File', Path', Rel, relfile)
|
||||
import qualified StrongPath as SP
|
||||
import Wasp.AppSpec (AppSpec)
|
||||
import qualified Wasp.AppSpec as AS
|
||||
import qualified Wasp.AppSpec.Entity as AS.Entity
|
||||
import Wasp.Generator.Common (ProjectRootDir, latestMajorNodeVersion)
|
||||
import Wasp.Generator.Common
|
||||
( ProjectRootDir,
|
||||
ServerRootDir,
|
||||
latestMajorNodeVersion,
|
||||
)
|
||||
import Wasp.Generator.DbGenerator.Common
|
||||
( PrismaDbSchema,
|
||||
dbSchemaFileFromAppComponentDir,
|
||||
serverPrismaClientOutputDirEnv,
|
||||
)
|
||||
import Wasp.Generator.FileDraft (FileDraft (..), createTemplateFileDraft)
|
||||
import qualified Wasp.Generator.FileDraft.TemplateFileDraft as TmplFD
|
||||
import Wasp.Generator.Monad (Generator, GeneratorError, runGenerator)
|
||||
import Wasp.Generator.ServerGenerator (areServerPatchesUsed)
|
||||
import Wasp.Generator.Templates (TemplatesDir, compileAndRenderTemplate)
|
||||
import qualified Wasp.SemanticVersion as SV
|
||||
import Wasp.Util (getEnvVarDefinition)
|
||||
|
||||
genDockerFiles :: AppSpec -> Generator [FileDraft]
|
||||
genDockerFiles spec = sequence [genDockerfile spec, genDockerignore spec]
|
||||
@ -30,6 +41,7 @@ genDockerFiles spec = sequence [genDockerfile spec, genDockerignore spec]
|
||||
genDockerfile :: AppSpec -> Generator FileDraft
|
||||
genDockerfile spec = do
|
||||
usingServerPatches <- areServerPatchesUsed spec
|
||||
let dbSchemaFileFromServerDir :: Path' (Rel ServerRootDir) (File PrismaDbSchema) = dbSchemaFileFromAppComponentDir
|
||||
return $
|
||||
createTemplateFileDraft
|
||||
([relfile|Dockerfile|] :: Path' (Rel ProjectRootDir) File')
|
||||
@ -37,6 +49,8 @@ genDockerfile spec = do
|
||||
( Just $
|
||||
object
|
||||
[ "usingPrisma" .= not (null $ AS.getDecls @AS.Entity.Entity spec),
|
||||
"serverPrismaClientOutputDirEnv" .= getEnvVarDefinition serverPrismaClientOutputDirEnv,
|
||||
"dbSchemaFileFromServerDir" .= SP.fromRelFile dbSchemaFileFromServerDir,
|
||||
"nodeMajorVersion" .= show (SV.major latestMajorNodeVersion),
|
||||
"usingServerPatches" .= usingServerPatches,
|
||||
"userDockerfile" .= fromMaybe "" (AS.userDockerfileContents spec)
|
||||
|
@ -11,7 +11,8 @@ where
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Text (Text)
|
||||
import StrongPath (Abs, Dir', File', Path', Rel)
|
||||
import StrongPath (Abs, Dir, File, Path', Rel)
|
||||
import qualified StrongPath as SP
|
||||
import Wasp.Generator.Common (ProjectRootDir)
|
||||
import qualified Wasp.Generator.FileDraft.CopyDirFileDraft as CopyDirFD
|
||||
import qualified Wasp.Generator.FileDraft.CopyFileDraft as CopyFD
|
||||
@ -51,44 +52,44 @@ instance Writeable FileDraft where
|
||||
getDstPath (FileDraftTextFd draft) = getDstPath draft
|
||||
|
||||
createTemplateFileDraft ::
|
||||
Path' (Rel ProjectRootDir) File' ->
|
||||
Path' (Rel TemplatesDir) File' ->
|
||||
Path' (Rel ProjectRootDir) (File a) ->
|
||||
Path' (Rel TemplatesDir) (File b) ->
|
||||
Maybe Aeson.Value ->
|
||||
FileDraft
|
||||
createTemplateFileDraft dstPath tmplSrcPath tmplData =
|
||||
FileDraftTemplateFd $
|
||||
TmplFD.TemplateFileDraft
|
||||
{ TmplFD._dstPath = dstPath,
|
||||
TmplFD._srcPathInTmplDir = tmplSrcPath,
|
||||
{ TmplFD._dstPath = SP.castFile dstPath,
|
||||
TmplFD._srcPathInTmplDir = SP.castFile tmplSrcPath,
|
||||
TmplFD._tmplData = tmplData
|
||||
}
|
||||
|
||||
createCopyFileDraft :: Path' (Rel ProjectRootDir) File' -> Path' Abs File' -> FileDraft
|
||||
createCopyFileDraft :: Path' (Rel ProjectRootDir) (File a) -> Path' Abs (File b) -> FileDraft
|
||||
createCopyFileDraft dstPath srcPath =
|
||||
FileDraftCopyFd $
|
||||
CopyFD.CopyFileDraft
|
||||
{ CopyFD._dstPath = dstPath,
|
||||
CopyFD._srcPath = srcPath,
|
||||
{ CopyFD._dstPath = SP.castFile dstPath,
|
||||
CopyFD._srcPath = SP.castFile srcPath,
|
||||
CopyFD._failIfSrcDoesNotExist = True
|
||||
}
|
||||
|
||||
createCopyFileDraftIfExists :: Path' (Rel ProjectRootDir) File' -> Path' Abs File' -> FileDraft
|
||||
createCopyFileDraftIfExists :: Path' (Rel ProjectRootDir) (File a) -> Path' Abs (File b) -> FileDraft
|
||||
createCopyFileDraftIfExists dstPath srcPath =
|
||||
FileDraftCopyFd $
|
||||
CopyFD.CopyFileDraft
|
||||
{ CopyFD._dstPath = dstPath,
|
||||
CopyFD._srcPath = srcPath,
|
||||
{ CopyFD._dstPath = SP.castFile dstPath,
|
||||
CopyFD._srcPath = SP.castFile srcPath,
|
||||
CopyFD._failIfSrcDoesNotExist = False
|
||||
}
|
||||
|
||||
createCopyDirFileDraft :: Path' (Rel ProjectRootDir) Dir' -> Path' Abs Dir' -> FileDraft
|
||||
createCopyDirFileDraft :: Path' (Rel ProjectRootDir) (Dir a) -> Path' Abs (Dir b) -> FileDraft
|
||||
createCopyDirFileDraft dstPath srcPath =
|
||||
FileDraftCopyDirFd $
|
||||
CopyDirFD.CopyDirFileDraft
|
||||
{ CopyDirFD._dstPath = dstPath,
|
||||
CopyDirFD._srcPath = srcPath
|
||||
{ CopyDirFD._dstPath = SP.castDir dstPath,
|
||||
CopyDirFD._srcPath = SP.castDir srcPath
|
||||
}
|
||||
|
||||
createTextFileDraft :: Path' (Rel ProjectRootDir) File' -> Text -> FileDraft
|
||||
createTextFileDraft :: Path' (Rel ProjectRootDir) (File a) -> Text -> FileDraft
|
||||
createTextFileDraft dstPath content =
|
||||
FileDraftTextFd $ TextFD.TextFileDraft {TextFD._dstPath = dstPath, TextFD._content = content}
|
||||
FileDraftTextFd $ TextFD.TextFileDraft {TextFD._dstPath = SP.castFile dstPath, TextFD._content = content}
|
||||
|
@ -3,6 +3,7 @@
|
||||
module Wasp.Generator.Job.Process
|
||||
( runProcessAsJob,
|
||||
runNodeCommandAsJob,
|
||||
runNodeCommandAsJobWithExtraEnv,
|
||||
parseNodeVersion,
|
||||
)
|
||||
where
|
||||
@ -16,6 +17,7 @@ import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
import StrongPath (Abs, Dir, Path')
|
||||
import qualified StrongPath as SP
|
||||
import System.Environment (getEnvironment)
|
||||
import System.Exit (ExitCode (..))
|
||||
import System.IO.Error (catchIOError, isDoesNotExistError)
|
||||
import qualified System.Info
|
||||
@ -95,20 +97,21 @@ runProcessAsJob process jobType chan =
|
||||
return $ ExitFailure 1
|
||||
|
||||
runNodeCommandAsJob :: Path' Abs (Dir a) -> String -> [String] -> J.JobType -> J.Job
|
||||
runNodeCommandAsJob fromDir command args jobType chan = do
|
||||
errorOrNodeVersion <- getNodeVersion
|
||||
case errorOrNodeVersion of
|
||||
runNodeCommandAsJob = runNodeCommandAsJobWithExtraEnv []
|
||||
|
||||
runNodeCommandAsJobWithExtraEnv :: [(String, String)] -> Path' Abs (Dir a) -> String -> [String] -> J.JobType -> J.Job
|
||||
runNodeCommandAsJobWithExtraEnv extraEnvVars fromDir command args jobType chan =
|
||||
getNodeVersion >>= \case
|
||||
Left errorMsg -> exitWithError (ExitFailure 1) (T.pack errorMsg)
|
||||
Right nodeVersion ->
|
||||
if SV.isVersionInRange nodeVersion C.nodeVersionRange
|
||||
then do
|
||||
let process = (P.proc command args) {P.cwd = Just $ SP.fromAbsDir fromDir}
|
||||
runProcessAsJob process jobType chan
|
||||
else
|
||||
exitWithError
|
||||
(ExitFailure 1)
|
||||
(T.pack $ makeNodeVersionMismatchMessage nodeVersion)
|
||||
envVars <- getAllEnvVars
|
||||
let nodeCommandProcess = (P.proc command args) {P.env = Just envVars, P.cwd = Just $ SP.fromAbsDir fromDir}
|
||||
runProcessAsJob nodeCommandProcess jobType chan
|
||||
else exitWithError (ExitFailure 1) (T.pack $ makeNodeVersionMismatchMessage nodeVersion)
|
||||
where
|
||||
getAllEnvVars = (++ extraEnvVars) <$> getEnvironment
|
||||
exitWithError exitCode errorMsg = do
|
||||
writeChan chan $
|
||||
J.JobMessage
|
||||
|
@ -36,7 +36,14 @@ 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.Generator.Common (latestMajorNodeVersion, nodeVersionRange, npmVersionRange, prismaVersion)
|
||||
import Wasp.Generator.Common
|
||||
( ServerRootDir,
|
||||
latestMajorNodeVersion,
|
||||
makeJsonWithEntityNameAndPrismaIdentifier,
|
||||
nodeVersionRange,
|
||||
npmVersionRange,
|
||||
prismaVersion,
|
||||
)
|
||||
import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir)
|
||||
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
|
||||
import Wasp.Generator.FileDraft (FileDraft, createCopyFileDraft)
|
||||
@ -85,7 +92,7 @@ genDotEnv spec = return $
|
||||
]
|
||||
_ -> []
|
||||
|
||||
dotEnvInServerRootDir :: Path' (Rel C.ServerRootDir) File'
|
||||
dotEnvInServerRootDir :: Path' (Rel ServerRootDir) File'
|
||||
dotEnvInServerRootDir = [relfile|.env|]
|
||||
|
||||
genPackageJson :: AppSpec -> N.NpmDepsForWasp -> Generator FileDraft
|
||||
@ -257,7 +264,7 @@ genTypesAndEntitiesDirs spec = return [entitiesIndexFileDraft, typesIndexFileDra
|
||||
"userViewName" .= fromMaybe "" userViewName
|
||||
]
|
||||
)
|
||||
allEntities = map (C.buildEntityData . fst) $ AS.getDecls @AS.Entity.Entity spec
|
||||
allEntities = map (makeJsonWithEntityNameAndPrismaIdentifier . fst) $ AS.getDecls @AS.Entity.Entity spec
|
||||
userEntityName = AS.refName . AS.App.Auth.userEntity <$> AS.App.auth (snd $ getApp spec)
|
||||
-- We might want to move this to a more global location in the future, but
|
||||
-- it is currently used only in these two files.
|
||||
|
@ -11,29 +11,22 @@ module Wasp.Generator.ServerGenerator.Common
|
||||
asTmplSrcFile,
|
||||
asServerFile,
|
||||
asServerSrcFile,
|
||||
entityNameToPrismaIdentifier,
|
||||
buildEntityData,
|
||||
toESModulesImportPath,
|
||||
ServerRootDir,
|
||||
ServerSrcDir,
|
||||
ServerTemplatesDir,
|
||||
ServerTemplatesSrcDir,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson (object, (.=))
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Char (toLower)
|
||||
import StrongPath (Dir, File', Path', Rel, reldir, relfile, (</>))
|
||||
import qualified StrongPath as SP
|
||||
import System.FilePath (splitExtension)
|
||||
import Wasp.Common (WaspProjectDir)
|
||||
import Wasp.Generator.Common (ProjectRootDir)
|
||||
import Wasp.Generator.Common (ProjectRootDir, ServerRootDir)
|
||||
import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft)
|
||||
import Wasp.Generator.Templates (TemplatesDir)
|
||||
|
||||
data ServerRootDir
|
||||
|
||||
data ServerSrcDir
|
||||
|
||||
data ServerTemplatesDir
|
||||
@ -97,20 +90,6 @@ srcDirInServerTemplatesDir = [reldir|src|]
|
||||
dotEnvServer :: Path' (SP.Rel WaspProjectDir) File'
|
||||
dotEnvServer = [relfile|.env.server|]
|
||||
|
||||
-- | Takes a Wasp Entity name (like `SomeTask` from `entity SomeTask {...}`) and
|
||||
-- converts it into a corresponding Prisma identifier (like `prisma.someTask`).
|
||||
-- This is what Prisma implicitly does when translating `model` declarations to
|
||||
-- client SDK identifiers. Useful when creating `context.entities` JS objects in Wasp templates.
|
||||
entityNameToPrismaIdentifier :: String -> String
|
||||
entityNameToPrismaIdentifier entityName = toLower (head entityName) : tail entityName
|
||||
|
||||
buildEntityData :: String -> Aeson.Value
|
||||
buildEntityData name =
|
||||
object
|
||||
[ "name" .= name,
|
||||
"prismaIdentifier" .= entityNameToPrismaIdentifier name
|
||||
]
|
||||
|
||||
-- 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
|
||||
|
@ -9,6 +9,7 @@ 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
|
||||
@ -20,7 +21,8 @@ genConfigFile spec = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tm
|
||||
dstFile = C.serverSrcDirInServerRootDir </> configFileInSrcDir
|
||||
tmplData =
|
||||
object
|
||||
[ "isAuthEnabled" .= (isAuthEnabled spec :: Bool)
|
||||
[ "isAuthEnabled" .= isAuthEnabled spec,
|
||||
"databaseUrlEnvVar" .= databaseUrlEnvVar
|
||||
]
|
||||
|
||||
configFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
|
||||
|
@ -32,13 +32,13 @@ import qualified Wasp.AppSpec.JSON as AS.JSON
|
||||
import Wasp.AppSpec.Job (Job, JobExecutor (PgBoss, Simple), jobExecutors)
|
||||
import qualified Wasp.AppSpec.Job as J
|
||||
import Wasp.AppSpec.Util (isPgBossJobExecutorUsed)
|
||||
import Wasp.Generator.Common (ServerRootDir, makeJsonWithEntityNameAndPrismaIdentifier)
|
||||
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
|
||||
import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport)
|
||||
import Wasp.Generator.Monad (Generator)
|
||||
import Wasp.Generator.ServerGenerator.Common
|
||||
( ServerRootDir,
|
||||
ServerSrcDir,
|
||||
( ServerSrcDir,
|
||||
ServerTemplatesDir,
|
||||
srcDirInServerTemplatesDir,
|
||||
)
|
||||
@ -65,7 +65,7 @@ genJob (jobName, job) =
|
||||
"jobSchedule" .= Aeson.Text.encodeToLazyText (fromMaybe Aeson.Null maybeJobSchedule),
|
||||
"jobPerformOptions" .= show (fromMaybe AS.JSON.emptyObject maybeJobPerformOptions),
|
||||
"executorJobRelFP" .= toFilePath (executorJobTemplateInJobsDir (J.executor job)),
|
||||
"entities" .= maybe [] (map (C.buildEntityData . AS.refName)) (J.entities job)
|
||||
"entities" .= maybe [] (map (makeJsonWithEntityNameAndPrismaIdentifier . AS.refName)) (J.entities job)
|
||||
]
|
||||
)
|
||||
where
|
||||
|
@ -21,6 +21,7 @@ import Wasp.AppSpec.Operation (getName)
|
||||
import qualified Wasp.AppSpec.Operation as AS.Operation
|
||||
import qualified Wasp.AppSpec.Query as AS.Query
|
||||
import Wasp.AppSpec.Valid (isAuthEnabled)
|
||||
import Wasp.Generator.Common (ServerRootDir, makeJsonWithEntityNameAndPrismaIdentifier)
|
||||
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
|
||||
import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport)
|
||||
@ -73,7 +74,7 @@ genQuery (queryName, query) = return $ C.mkTmplFdWithDstAndData tmplFile dstFile
|
||||
|
||||
genOperationTypesFile ::
|
||||
Path' (Rel C.ServerTemplatesDir) File' ->
|
||||
Path' (Rel C.ServerRootDir) File' ->
|
||||
Path' (Rel ServerRootDir) File' ->
|
||||
[AS.Operation.Operation] ->
|
||||
Bool ->
|
||||
Generator FileDraft
|
||||
@ -130,7 +131,11 @@ operationTmplData operation =
|
||||
object
|
||||
[ "jsFnImportStatement" .= importStmt,
|
||||
"jsFnIdentifier" .= importIdentifier,
|
||||
"entities" .= maybe [] (map (C.buildEntityData . AS.refName)) (AS.Operation.getEntities operation)
|
||||
"entities"
|
||||
.= maybe
|
||||
[]
|
||||
(map (makeJsonWithEntityNameAndPrismaIdentifier . AS.refName))
|
||||
(AS.Operation.getEntities operation)
|
||||
]
|
||||
where
|
||||
(importIdentifier, importStmt) =
|
||||
|
@ -19,6 +19,7 @@ import qualified Wasp.AppSpec.App.Auth as AS.Auth
|
||||
import qualified Wasp.AppSpec.Operation as AS.Operation
|
||||
import qualified Wasp.AppSpec.Query as AS.Query
|
||||
import Wasp.AppSpec.Valid (getApp, isAuthEnabled)
|
||||
import Wasp.Generator.Common (ServerRootDir)
|
||||
import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.Monad (Generator, GeneratorError (GenericGeneratorError), logAndThrowGeneratorError)
|
||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||
@ -75,7 +76,7 @@ data OperationsRoutesDir
|
||||
operationsRoutesDirInServerSrcDir :: Path' (Rel C.ServerSrcDir) (Dir OperationsRoutesDir)
|
||||
operationsRoutesDirInServerSrcDir = [reldir|routes/operations/|]
|
||||
|
||||
operationsRoutesDirInServerRootDir :: Path' (Rel C.ServerRootDir) (Dir OperationsRoutesDir)
|
||||
operationsRoutesDirInServerRootDir :: Path' (Rel ServerRootDir) (Dir OperationsRoutesDir)
|
||||
operationsRoutesDirInServerRootDir = C.serverSrcDirInServerRootDir </> operationsRoutesDirInServerSrcDir
|
||||
|
||||
operationRouteFileInOperationsRoutesDir :: AS.Operation.Operation -> Path' (Rel OperationsRoutesDir) File'
|
||||
|
@ -1,3 +1,5 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Wasp.Generator.WebAppGenerator
|
||||
( genWebApp,
|
||||
npmDepsForWasp,
|
||||
@ -25,8 +27,14 @@ 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.Dependency as AS.Dependency
|
||||
import qualified Wasp.AppSpec.Entity as AS.Entity
|
||||
import Wasp.AppSpec.Valid (getApp)
|
||||
import Wasp.Generator.Common (nodeVersionRange, npmVersionRange)
|
||||
import Wasp.Generator.Common
|
||||
( makeJsonWithEntityNameAndPrismaIdentifier,
|
||||
nodeVersionRange,
|
||||
npmVersionRange,
|
||||
prismaVersion,
|
||||
)
|
||||
import qualified Wasp.Generator.ConfigFile as G.CF
|
||||
import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir)
|
||||
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
|
||||
@ -113,7 +121,11 @@ npmDepsForWasp spec =
|
||||
("react-dom", "^17.0.2"),
|
||||
("@tanstack/react-query", "^4.13.0"),
|
||||
("react-router-dom", "^5.3.3"),
|
||||
("react-scripts", "5.0.1")
|
||||
("react-scripts", "5.0.1"),
|
||||
-- The web app only needs @prisma/client (we're using the server's
|
||||
-- CLI to generate what's necessary, check the description in
|
||||
-- https://github.com/wasp-lang/wasp/pull/962/ for details).
|
||||
("@prisma/client", show prismaVersion)
|
||||
]
|
||||
++ depsRequiredByTailwind spec,
|
||||
-- NOTE: In order to follow Create React App conventions, do not place any dependencies under devDependencies.
|
||||
@ -198,8 +210,6 @@ genPublicIndexHtml spec =
|
||||
-- TODO(matija): Currently we also generate auth-specific parts in this file (e.g. token management),
|
||||
-- although they are not used anywhere outside.
|
||||
-- We could further "templatize" this file so only what is needed is generated.
|
||||
--
|
||||
|
||||
genSrcDir :: AppSpec -> Generator [FileDraft]
|
||||
genSrcDir spec =
|
||||
sequence
|
||||
@ -213,10 +223,21 @@ genSrcDir spec =
|
||||
genApi
|
||||
]
|
||||
<++> genOperations spec
|
||||
<++> genEntitiesDir spec
|
||||
<++> genAuth spec
|
||||
where
|
||||
copyTmplFile = return . C.mkSrcTmplFd
|
||||
|
||||
genEntitiesDir :: AppSpec -> Generator [FileDraft]
|
||||
genEntitiesDir spec = return [entitiesIndexFileDraft]
|
||||
where
|
||||
entitiesIndexFileDraft =
|
||||
C.mkTmplFdWithDstAndData
|
||||
[relfile|src/entities/index.ts|]
|
||||
[relfile|src/entities/index.ts|]
|
||||
(Just $ object ["entities" .= allEntities])
|
||||
allEntities = map (makeJsonWithEntityNameAndPrismaIdentifier . fst) $ AS.getDecls @AS.Entity.Entity spec
|
||||
|
||||
-- | Generates api.js file which contains token management and configured api (e.g. axios) instance.
|
||||
genApi :: Generator FileDraft
|
||||
genApi = return $ C.mkTmplFd (C.asTmplFile [relfile|src/api.js|])
|
||||
|
@ -23,12 +23,10 @@ import qualified Data.Aeson as Aeson
|
||||
import StrongPath (Dir, File', Path', Rel, reldir, relfile, (</>))
|
||||
import qualified StrongPath as SP
|
||||
import Wasp.Common (WaspProjectDir)
|
||||
import Wasp.Generator.Common (ProjectRootDir)
|
||||
import Wasp.Generator.Common (ProjectRootDir, WebAppRootDir)
|
||||
import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft)
|
||||
import Wasp.Generator.Templates (TemplatesDir)
|
||||
|
||||
data WebAppRootDir
|
||||
|
||||
data WebAppSrcDir
|
||||
|
||||
data WebAppTemplatesDir
|
||||
|
@ -21,7 +21,7 @@ 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, fromAbsFile, reldir, relfile, toFilePath, (</>))
|
||||
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)
|
||||
@ -44,7 +44,7 @@ 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 Util.IO
|
||||
import qualified Wasp.Util.IO as IOUtil
|
||||
|
||||
type CompileError = String
|
||||
|
||||
@ -90,7 +90,7 @@ warnIfDotEnvPresent waspDir = (warningMessage <$) <$> findDotEnv waspDir
|
||||
|
||||
analyzeWaspFileContent :: Path' Abs File' -> IO (Either CompileError [AS.Decl])
|
||||
analyzeWaspFileContent waspFilePath = do
|
||||
waspFileContent <- readFile (fromAbsFile waspFilePath)
|
||||
waspFileContent <- IOUtil.readFile waspFilePath
|
||||
let declsOrAnalyzeError = Analyzer.analyze waspFileContent
|
||||
return $
|
||||
left
|
||||
@ -134,7 +134,7 @@ constructAppSpec waspDir options decls = do
|
||||
|
||||
findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String (Path' Abs File'))
|
||||
findWaspFile waspDir = do
|
||||
files <- fst <$> Util.IO.listDirectory waspDir
|
||||
files <- fst <$> IOUtil.listDirectory waspDir
|
||||
return $ maybeToEither "Couldn't find a single *.wasp file." $ (waspDir </>) <$> find isWaspFile files
|
||||
where
|
||||
isWaspFile path =
|
||||
|
@ -4,6 +4,7 @@ module Wasp.Util
|
||||
( Checksum,
|
||||
camelToKebabCase,
|
||||
checksumFromString,
|
||||
getEnvVarDefinition,
|
||||
checksumFromText,
|
||||
checksumFromByteString,
|
||||
onFirst,
|
||||
@ -48,6 +49,8 @@ import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.Text.Encoding as TextEncoding
|
||||
import StrongPath (File, Path')
|
||||
import qualified StrongPath as SP
|
||||
import Text.Printf (printf)
|
||||
|
||||
camelToKebabCase :: String -> String
|
||||
@ -191,8 +194,8 @@ checksumFromText = bytestringToHex . SHA256.hash . TextEncoding.encodeUtf8
|
||||
checksumFromByteString :: BSU.ByteString -> Checksum
|
||||
checksumFromByteString = bytestringToHex . SHA256.hash
|
||||
|
||||
checksumFromFilePath :: FilePath -> IO Checksum
|
||||
checksumFromFilePath file = checksumFromByteString <$> B.readFile file
|
||||
checksumFromFilePath :: Path' r (File f) -> IO Checksum
|
||||
checksumFromFilePath = fmap checksumFromByteString . B.readFile . SP.toFilePath
|
||||
|
||||
checksumFromChecksums :: [Checksum] -> Checksum
|
||||
checksumFromChecksums = checksumFromString . concatMap (\(Hex s) -> s)
|
||||
@ -220,3 +223,6 @@ orIfNothingM = flip fromMaybeM
|
||||
|
||||
maybeToEither :: a -> Maybe b -> Either a b
|
||||
maybeToEither leftValue = maybe (Left leftValue) Right
|
||||
|
||||
getEnvVarDefinition :: (String, String) -> String
|
||||
getEnvVarDefinition (name, value) = concat [name, "=", value]
|
||||
|
@ -5,16 +5,23 @@ module Wasp.Util.IO
|
||||
listDirectory,
|
||||
deleteDirectoryIfExists,
|
||||
deleteFileIfExists,
|
||||
doesFileExist,
|
||||
readFile,
|
||||
writeFile,
|
||||
removeFile,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (filterM, when)
|
||||
import Control.Monad.Extra (whenM)
|
||||
import StrongPath (Abs, Dir, Dir', File, Path', Rel, basename, parseRelDir, parseRelFile, toFilePath, (</>))
|
||||
import qualified StrongPath as SP
|
||||
import qualified System.Directory as SD
|
||||
import qualified System.FilePath as FilePath
|
||||
import System.IO.Error (isDoesNotExistError)
|
||||
import UnliftIO.Exception (catch, throwIO)
|
||||
import Prelude hiding (readFile, writeFile)
|
||||
import qualified Prelude as P
|
||||
|
||||
-- TODO: write tests.
|
||||
|
||||
@ -63,14 +70,28 @@ listDirectory absDirPath = do
|
||||
filterM (SD.doesDirectoryExist . (absDir FilePath.</>)) relItems
|
||||
>>= mapM parseRelDir
|
||||
|
||||
deleteDirectoryIfExists :: Path' r (Dir d) -> IO ()
|
||||
-- The paths in the following functions intentionally aren't as polymorphic as
|
||||
-- possible (i.e., they require 'Abs` paths). We prefer working with absolute
|
||||
-- paths whenever possible (they make for a safe default). If you need to work
|
||||
-- with relative paths, define a new function (e.g., `readFileRel`).
|
||||
|
||||
deleteDirectoryIfExists :: Path' Abs (Dir d) -> IO ()
|
||||
deleteDirectoryIfExists dirPath = do
|
||||
let dirPathStr = SP.toFilePath dirPath
|
||||
let dirPathStr = SP.fromAbsDir dirPath
|
||||
exists <- SD.doesDirectoryExist dirPathStr
|
||||
when exists $ SD.removeDirectoryRecursive dirPathStr
|
||||
|
||||
deleteFileIfExists :: Path' r (File f) -> IO ()
|
||||
deleteFileIfExists filePath = do
|
||||
let filePathStr = SP.toFilePath filePath
|
||||
exists <- SD.doesFileExist filePathStr
|
||||
when exists $ SD.removeFile filePathStr
|
||||
deleteFileIfExists :: Path' Abs (File f) -> IO ()
|
||||
deleteFileIfExists filePath = whenM (doesFileExist filePath) $ removeFile filePath
|
||||
|
||||
doesFileExist :: Path' Abs (File f) -> IO Bool
|
||||
doesFileExist = SD.doesFileExist . SP.fromAbsFile
|
||||
|
||||
readFile :: Path' Abs (File f) -> IO String
|
||||
readFile = P.readFile . SP.fromAbsFile
|
||||
|
||||
writeFile :: Path' Abs (File f) -> String -> IO ()
|
||||
writeFile = P.writeFile . SP.fromAbsFile
|
||||
|
||||
removeFile :: Path' Abs (File f) -> IO ()
|
||||
removeFile = SD.removeFile . SP.fromAbsFile
|
||||
|
@ -7,10 +7,10 @@ module Wasp.WaspignoreFile
|
||||
where
|
||||
|
||||
import StrongPath (Abs, File', Path')
|
||||
import qualified StrongPath as SP
|
||||
import System.FilePath.Glob (Pattern, compile, match)
|
||||
import System.IO.Error (isDoesNotExistError)
|
||||
import UnliftIO.Exception (catch, throwIO)
|
||||
import qualified Wasp.Util.IO as IOUtil
|
||||
|
||||
newtype WaspignoreFile = WaspignoreFile [Pattern]
|
||||
|
||||
@ -53,9 +53,9 @@ parseWaspignoreFile =
|
||||
--
|
||||
-- If the ignore file does not exist, it is interpreted as a blank file.
|
||||
readWaspignoreFile :: Path' Abs File' -> IO WaspignoreFile
|
||||
readWaspignoreFile fp = do
|
||||
readWaspignoreFile file = do
|
||||
text <-
|
||||
readFile (SP.fromAbsFile fp)
|
||||
IOUtil.readFile file
|
||||
`catch` ( \e ->
|
||||
if isDoesNotExistError e
|
||||
then return ""
|
||||
|
Loading…
Reference in New Issue
Block a user