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:
Filip Sodić 2023-02-13 14:31:49 +01:00 committed by GitHub
parent e4e29eec0b
commit c41f065ef4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 686 additions and 349 deletions

View File

@ -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

View File

@ -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,

View File

@ -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).

View File

@ -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 =

View File

@ -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

View File

@ -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) =

View File

@ -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|]

View File

@ -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 =}

View File

@ -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 =}

View File

@ -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();

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -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",

View 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")
}

View File

@ -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"}]}}

View File

@ -12,6 +12,7 @@
]
},
"dependencies": {
"@prisma/client": "4.5.0",
"@tanstack/react-query": "^4.13.0",
"axios": "^0.27.2",
"react": "^17.0.2",

View File

@ -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();

View File

@ -0,0 +1,8 @@
import {
} from '@prisma/client'
export type {
} from '@prisma/client'
export type WaspEntity =
| never

View File

@ -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

View File

@ -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",

View 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")
}

View File

@ -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"}]}}

View File

@ -12,6 +12,7 @@
]
},
"dependencies": {
"@prisma/client": "4.5.0",
"@tanstack/react-query": "^4.13.0",
"axios": "^0.27.2",
"react": "^17.0.2",

View File

@ -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();

View File

@ -0,0 +1,8 @@
import {
} from '@prisma/client'
export type {
} from '@prisma/client'
export type WaspEntity =
| never

View File

@ -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

View File

@ -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",

View 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")
}

View File

@ -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"}]}}

View File

@ -12,6 +12,7 @@
]
},
"dependencies": {
"@prisma/client": "4.5.0",
"@tanstack/react-query": "^4.13.0",
"axios": "^0.27.2",
"react": "^17.0.2",

View File

@ -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();

View File

@ -0,0 +1,8 @@
import {
} from '@prisma/client'
export type {
} from '@prisma/client'
export type WaspEntity =
| never

View File

@ -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

View File

@ -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",

View 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?

View File

@ -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 {

View File

@ -1 +1 @@
04a6c677bba33a37c60132e09b60fec7679403fed77ad4cb7d7c306a600cef24
8d017edd849a861ae086850270a9f817bb4b75d9ee9ac27c08b0e9c29a16f6fe

View File

@ -1 +1 @@
04a6c677bba33a37c60132e09b60fec7679403fed77ad4cb7d7c306a600cef24
8d017edd849a861ae086850270a9f817bb4b75d9ee9ac27c08b0e9c29a16f6fe

View File

@ -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"}]}}

View File

@ -12,6 +12,7 @@
]
},
"dependencies": {
"@prisma/client": "4.5.0",
"@tanstack/react-query": "^4.13.0",
"axios": "^0.27.2",
"react": "^17.0.2",

View File

@ -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();

View File

@ -0,0 +1,11 @@
import {
Task,
} from '@prisma/client'
export type {
Task,
} from '@prisma/client'
export type WaspEntity =
| Task
| never

View File

@ -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)

View File

@ -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>
</>

View File

@ -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 />

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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|]

View File

@ -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.

View File

@ -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)

View File

@ -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}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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'

View 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

View File

@ -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) =

View File

@ -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'

View 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|])

View File

@ -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

View File

@ -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 =

View File

@ -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]

View File

@ -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

View File

@ -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 ""