mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-09-11 07:16:09 +03:00
Adds new complex e2e test (#1030)
This commit is contained in:
parent
6dcfb290d9
commit
bbaf8bdf3b
@ -3,6 +3,7 @@ import System.Info (os)
|
||||
import Test.Tasty (TestTree, defaultMain, testGroup)
|
||||
import Tests.WaspBuildTest (waspBuild)
|
||||
import Tests.WaspCompileTest (waspCompile)
|
||||
import Tests.WaspComplexTest (waspComplexTest)
|
||||
import Tests.WaspJobTest (waspJob)
|
||||
import Tests.WaspMigrateTest (waspMigrate)
|
||||
import Tests.WaspNewTest (waspNew)
|
||||
@ -25,5 +26,6 @@ tests = do
|
||||
waspCompile,
|
||||
waspMigrate,
|
||||
waspBuild,
|
||||
waspJob
|
||||
waspJob,
|
||||
waspComplexTest
|
||||
]
|
||||
|
@ -1,4 +1,5 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
|
||||
module ShellCommands
|
||||
( ShellCommand,
|
||||
@ -15,6 +16,7 @@ module ShellCommands
|
||||
waspCliMigrate,
|
||||
waspCliBuild,
|
||||
dockerBuild,
|
||||
insertCodeIntoWaspFileAtLineNumber,
|
||||
)
|
||||
where
|
||||
|
||||
@ -71,11 +73,14 @@ createFile content relDirFp filename = return $ combineShellCommands [createPare
|
||||
-- NOTE: This is fragile and will likely break in future. Assumes `app` decl is first line and by default
|
||||
-- we do not have a `db` field. Consider better alternatives.
|
||||
setDbToPSQL :: ShellCommandBuilder ShellCommand
|
||||
setDbToPSQL =
|
||||
-- Change DB to postgres by adding string at specific line so it still parses.
|
||||
setDbToPSQL = insertCodeIntoWaspFileAtLineNumber 2 " db: { system: PostgreSQL },"
|
||||
|
||||
insertCodeIntoWaspFileAtLineNumber :: Int -> String -> ShellCommandBuilder ShellCommand
|
||||
insertCodeIntoWaspFileAtLineNumber atLineNumber line =
|
||||
return $
|
||||
combineShellCommands
|
||||
[ -- Change DB to postgres by adding string at specific line so it still parses.
|
||||
"awk 'NR==2{print \" db: { system: PostgreSQL },\"}1' main.wasp > main.wasp.tmp",
|
||||
[ "awk 'NR==" ++ show atLineNumber ++ "{print " ++ show line ++ "}1' main.wasp > main.wasp.tmp",
|
||||
"mv main.wasp.tmp main.wasp"
|
||||
]
|
||||
|
||||
|
230
waspc/e2e-test/Tests/WaspComplexTest.hs
Normal file
230
waspc/e2e-test/Tests/WaspComplexTest.hs
Normal file
@ -0,0 +1,230 @@
|
||||
module Tests.WaspComplexTest (waspComplexTest) where
|
||||
|
||||
import GoldenTest (GoldenTest, makeGoldenTest)
|
||||
import ShellCommands
|
||||
( ShellCommand,
|
||||
ShellCommandBuilder,
|
||||
appendToWaspFile,
|
||||
cdIntoCurrentProject,
|
||||
createFile,
|
||||
insertCodeIntoWaspFileAtLineNumber,
|
||||
setDbToPSQL,
|
||||
waspCliCompile,
|
||||
waspCliNew,
|
||||
)
|
||||
import Util ((<++>))
|
||||
|
||||
waspComplexTest :: GoldenTest
|
||||
waspComplexTest = do
|
||||
makeGoldenTest "waspComplexTest" $
|
||||
sequence
|
||||
[ waspCliNew,
|
||||
cdIntoCurrentProject
|
||||
]
|
||||
<++> addServerEnvFile
|
||||
<++> addDependencies
|
||||
<++> addClientSetup
|
||||
<++> addServerSetup
|
||||
<++> addGoogleAuth
|
||||
<++> addJob
|
||||
<++> addAction
|
||||
<++> addQuery
|
||||
<++> sequence
|
||||
[ waspCliCompile
|
||||
]
|
||||
|
||||
addClientSetup :: ShellCommandBuilder [ShellCommand]
|
||||
addClientSetup = do
|
||||
sequence
|
||||
[ createFile rootComponentContent "./src/client" "App.jsx",
|
||||
createFile clientSetupFnContent "./src/client" "myClientSetupCode.js",
|
||||
insertCodeIntoWaspFileAfterVersion clientField
|
||||
]
|
||||
where
|
||||
clientField =
|
||||
unlines
|
||||
[ " client: {",
|
||||
" setupFn: import myClientSetupFunction from \"@client/myClientSetupCode.js\",",
|
||||
" rootComponent: import App from \"@client/App.jsx\"",
|
||||
" },"
|
||||
]
|
||||
rootComponentContent =
|
||||
unlines
|
||||
[ "export default function App({ children }) {",
|
||||
" return (",
|
||||
" <div className=\"app\">",
|
||||
" <h1>Root component</h1>",
|
||||
" {children}",
|
||||
" </div>",
|
||||
" );",
|
||||
"}"
|
||||
]
|
||||
|
||||
clientSetupFnContent =
|
||||
unlines
|
||||
[ "export default function myClientSetupFunction() {",
|
||||
" // Do some client setup here.",
|
||||
"}"
|
||||
]
|
||||
|
||||
addServerSetup :: ShellCommandBuilder [ShellCommand]
|
||||
addServerSetup = do
|
||||
sequence
|
||||
[ createFile serverSetupFnContent "./src/server" "myServerSetupCode.js",
|
||||
insertCodeIntoWaspFileAfterVersion serverField
|
||||
]
|
||||
where
|
||||
serverField =
|
||||
unlines
|
||||
[ " server: {",
|
||||
" setupFn: import mySetupFunction from \"@server/myServerSetupCode.js\",",
|
||||
" },"
|
||||
]
|
||||
serverSetupFnContent =
|
||||
unlines
|
||||
[ "export default function mySetupFunction() {",
|
||||
" // Do some server setup here.",
|
||||
"}"
|
||||
]
|
||||
|
||||
addJob :: ShellCommandBuilder [ShellCommand]
|
||||
addJob = do
|
||||
sequence
|
||||
[ setDbToPSQL,
|
||||
appendToWaspFile jobDecl,
|
||||
createFile jobFile "./src/server/jobs" "bar.js"
|
||||
]
|
||||
where
|
||||
jobDecl =
|
||||
unlines
|
||||
[ "job MySpecialJob {",
|
||||
" executor: PgBoss,",
|
||||
" perform: {",
|
||||
" fn: import { foo } from \"@server/jobs/bar.js\"",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
|
||||
jobFile =
|
||||
unlines
|
||||
[ "export const foo = async (args) => {",
|
||||
" return 1",
|
||||
"}"
|
||||
]
|
||||
|
||||
addServerEnvFile :: ShellCommandBuilder [ShellCommand]
|
||||
addServerEnvFile = do
|
||||
sequence [createFile envFileContents "./" ".env.server"]
|
||||
where
|
||||
envFileContents =
|
||||
unlines
|
||||
[ "GOOGLE_CLIENT_ID=google_client_id",
|
||||
"GOOGLE_CLIENT_SECRET=google_client_secret"
|
||||
]
|
||||
|
||||
addGoogleAuth :: ShellCommandBuilder [ShellCommand]
|
||||
addGoogleAuth = do
|
||||
sequence
|
||||
[ insertCodeIntoWaspFileAfterVersion authField,
|
||||
appendToWaspFile userEntity,
|
||||
appendToWaspFile socialLoginEntity
|
||||
]
|
||||
where
|
||||
authField =
|
||||
unlines
|
||||
[ " auth: {",
|
||||
" userEntity: User,",
|
||||
" externalAuthEntity: SocialLogin,",
|
||||
" methods: {",
|
||||
" google: {}",
|
||||
" },",
|
||||
" onAuthFailedRedirectTo: \"/login\"",
|
||||
" },"
|
||||
]
|
||||
|
||||
userEntity =
|
||||
unlines
|
||||
[ "entity User {=psl",
|
||||
" id Int @id @default(autoincrement())",
|
||||
" username String @unique",
|
||||
" password String",
|
||||
" externalAuthAssociations SocialLogin[]",
|
||||
"psl=}"
|
||||
]
|
||||
|
||||
socialLoginEntity =
|
||||
unlines
|
||||
[ "entity SocialLogin {=psl",
|
||||
" id Int @id @default(autoincrement())",
|
||||
" provider String",
|
||||
" providerId String",
|
||||
" user User @relation(fields: [userId], references: [id], onDelete: Cascade)",
|
||||
" userId Int",
|
||||
" createdAt DateTime @default(now())",
|
||||
" @@unique([provider, providerId, userId])",
|
||||
"psl=}"
|
||||
]
|
||||
|
||||
addAction :: ShellCommandBuilder [ShellCommand]
|
||||
addAction = do
|
||||
sequence
|
||||
[ appendToWaspFile actionDecl,
|
||||
createFile actionFile "./src/server/actions" "bar.js"
|
||||
]
|
||||
where
|
||||
actionDecl =
|
||||
unlines
|
||||
[ "action MySpecialAction {",
|
||||
" fn: import { foo } from \"@server/actions/bar.js\",",
|
||||
" entities: [User],",
|
||||
"}"
|
||||
]
|
||||
|
||||
actionFile =
|
||||
unlines
|
||||
[ "export const foo = async (args) => {",
|
||||
" return 1",
|
||||
"}"
|
||||
]
|
||||
|
||||
addQuery :: ShellCommandBuilder [ShellCommand]
|
||||
addQuery = do
|
||||
sequence
|
||||
[ appendToWaspFile queryDecl,
|
||||
createFile queryFile "./src/server/queries" "bar.js"
|
||||
]
|
||||
where
|
||||
queryDecl =
|
||||
unlines
|
||||
[ "query MySpecialQuery {",
|
||||
" fn: import { foo } from \"@server/queries/bar.js\",",
|
||||
" entities: [User],",
|
||||
"}"
|
||||
]
|
||||
|
||||
queryFile =
|
||||
unlines
|
||||
[ "export const foo = async (args) => {",
|
||||
" return 1",
|
||||
"}"
|
||||
]
|
||||
|
||||
addDependencies :: ShellCommandBuilder [ShellCommand]
|
||||
addDependencies = do
|
||||
sequence
|
||||
[ insertCodeIntoWaspFileAfterVersion deps
|
||||
]
|
||||
where
|
||||
deps =
|
||||
unlines
|
||||
[ " dependencies: [",
|
||||
" (\"redux\", \"^4.0.5\"),",
|
||||
" (\"react-redux\", \"^7.1.3\")",
|
||||
" ],"
|
||||
]
|
||||
|
||||
insertCodeIntoWaspFileAfterVersion :: String -> ShellCommandBuilder ShellCommand
|
||||
insertCodeIntoWaspFileAfterVersion = insertCodeIntoWaspFileAtLineNumber lineNumberInWaspFileAfterVersion
|
||||
where
|
||||
lineNumberInWaspFileAfterVersion :: Int
|
||||
lineNumberInWaspFileAfterVersion = 5
|
@ -12,7 +12,7 @@ import ShellCommands
|
||||
|
||||
waspJob :: GoldenTest
|
||||
waspJob = do
|
||||
let entityDecl =
|
||||
let jobDecl =
|
||||
" job MySpecialJob { \n\
|
||||
\ executor: PgBoss, \n\
|
||||
\ perform: { \n\
|
||||
@ -30,7 +30,7 @@ waspJob = do
|
||||
[ waspCliNew,
|
||||
cdIntoCurrentProject,
|
||||
setDbToPSQL,
|
||||
appendToWaspFile entityDecl,
|
||||
appendToWaspFile jobDecl,
|
||||
createFile jobFile "./src/server/jobs" "bar.js",
|
||||
waspCliCompile
|
||||
]
|
||||
|
8
waspc/e2e-test/Util.hs
Normal file
8
waspc/e2e-test/Util.hs
Normal file
@ -0,0 +1,8 @@
|
||||
module Util where
|
||||
|
||||
import Control.Applicative (liftA2)
|
||||
|
||||
infixr 5 <++>
|
||||
|
||||
(<++>) :: Applicative f => f [a] -> f [a] -> f [a]
|
||||
(<++>) = liftA2 (++)
|
127
waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest
generated
Normal file
127
waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest
generated
Normal file
@ -0,0 +1,127 @@
|
||||
waspComplexTest/.env.server
|
||||
waspComplexTest/.wasp/out/.dockerignore
|
||||
waspComplexTest/.wasp/out/.waspchecksums
|
||||
waspComplexTest/.wasp/out/Dockerfile
|
||||
waspComplexTest/.wasp/out/db/package.json
|
||||
waspComplexTest/.wasp/out/db/schema.prisma
|
||||
waspComplexTest/.wasp/out/db/schema.prisma.wasp-generate-checksum
|
||||
waspComplexTest/.wasp/out/installedFullStackNpmDependencies.json
|
||||
waspComplexTest/.wasp/out/server/.env
|
||||
waspComplexTest/.wasp/out/server/.npmrc
|
||||
waspComplexTest/.wasp/out/server/README.md
|
||||
waspComplexTest/.wasp/out/server/nodemon.json
|
||||
waspComplexTest/.wasp/out/server/package.json
|
||||
waspComplexTest/.wasp/out/server/patches/oauth+0.9.15.patch
|
||||
waspComplexTest/.wasp/out/server/scripts/universal/validators.mjs
|
||||
waspComplexTest/.wasp/out/server/scripts/validate-env.mjs
|
||||
waspComplexTest/.wasp/out/server/src/_types/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/_types/taggedEntities.ts
|
||||
waspComplexTest/.wasp/out/server/src/actions/MySpecialAction.js
|
||||
waspComplexTest/.wasp/out/server/src/actions/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/app.js
|
||||
waspComplexTest/.wasp/out/server/src/config.js
|
||||
waspComplexTest/.wasp/out/server/src/core/AuthError.js
|
||||
waspComplexTest/.wasp/out/server/src/core/HttpError.js
|
||||
waspComplexTest/.wasp/out/server/src/core/auth.js
|
||||
waspComplexTest/.wasp/out/server/src/core/auth/prismaMiddleware.js
|
||||
waspComplexTest/.wasp/out/server/src/dbClient.js
|
||||
waspComplexTest/.wasp/out/server/src/entities/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/actions/bar.js
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/jobs/bar.js
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/myServerSetupCode.js
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/queries/bar.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/MySpecialJob.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/core/Job.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/core/SubmittedJob.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/core/allJobs.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/core/pgBoss/pgBoss.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/core/pgBoss/pgBossJob.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/core/simpleJob.js
|
||||
waspComplexTest/.wasp/out/server/src/queries/MySpecialQuery.js
|
||||
waspComplexTest/.wasp/out/server/src/queries/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/index.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/login.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/me.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/passport/generic/provider.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/passport/google/config.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/passport/google/configMapping.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/passport/google/defaults.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/passport/passport.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/signup.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/utils.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/index.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/operations/MySpecialAction.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/operations/MySpecialQuery.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/operations/index.js
|
||||
waspComplexTest/.wasp/out/server/src/server.ts
|
||||
waspComplexTest/.wasp/out/server/src/types/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/universal/url.ts
|
||||
waspComplexTest/.wasp/out/server/src/utils.js
|
||||
waspComplexTest/.wasp/out/server/tsconfig.json
|
||||
waspComplexTest/.wasp/out/web-app/.npmrc
|
||||
waspComplexTest/.wasp/out/web-app/README.md
|
||||
waspComplexTest/.wasp/out/web-app/index.html
|
||||
waspComplexTest/.wasp/out/web-app/netlify.toml
|
||||
waspComplexTest/.wasp/out/web-app/package.json
|
||||
waspComplexTest/.wasp/out/web-app/public/favicon.ico
|
||||
waspComplexTest/.wasp/out/web-app/public/images/google-logo-icon.png
|
||||
waspComplexTest/.wasp/out/web-app/public/manifest.json
|
||||
waspComplexTest/.wasp/out/web-app/scripts/universal/validators.mjs
|
||||
waspComplexTest/.wasp/out/web-app/scripts/validate-env.mjs
|
||||
waspComplexTest/.wasp/out/web-app/src/actions/MySpecialAction.js
|
||||
waspComplexTest/.wasp/out/web-app/src/actions/core.d.ts
|
||||
waspComplexTest/.wasp/out/web-app/src/actions/core.js
|
||||
waspComplexTest/.wasp/out/web-app/src/actions/index.ts
|
||||
waspComplexTest/.wasp/out/web-app/src/api.js
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/forms/Login.jsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/forms/Signup.jsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/helpers/Google.jsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/helpers/user.ts
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/login.js
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/logout.js
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/pages/OAuthCodeExchange.jsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/pages/createAuthRequiredPage.jsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/signup.js
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/useAuth.js
|
||||
waspComplexTest/.wasp/out/web-app/src/config.js
|
||||
waspComplexTest/.wasp/out/web-app/src/entities/index.ts
|
||||
waspComplexTest/.wasp/out/web-app/src/ext-src/App.jsx
|
||||
waspComplexTest/.wasp/out/web-app/src/ext-src/Main.css
|
||||
waspComplexTest/.wasp/out/web-app/src/ext-src/MainPage.jsx
|
||||
waspComplexTest/.wasp/out/web-app/src/ext-src/myClientSetupCode.js
|
||||
waspComplexTest/.wasp/out/web-app/src/ext-src/vite-env.d.ts
|
||||
waspComplexTest/.wasp/out/web-app/src/ext-src/waspLogo.png
|
||||
waspComplexTest/.wasp/out/web-app/src/index.tsx
|
||||
waspComplexTest/.wasp/out/web-app/src/logo.png
|
||||
waspComplexTest/.wasp/out/web-app/src/operations/index.js
|
||||
waspComplexTest/.wasp/out/web-app/src/operations/resources.js
|
||||
waspComplexTest/.wasp/out/web-app/src/operations/updateHandlersMap.js
|
||||
waspComplexTest/.wasp/out/web-app/src/queries/MySpecialQuery.js
|
||||
waspComplexTest/.wasp/out/web-app/src/queries/core.d.ts
|
||||
waspComplexTest/.wasp/out/web-app/src/queries/core.js
|
||||
waspComplexTest/.wasp/out/web-app/src/queries/index.d.ts
|
||||
waspComplexTest/.wasp/out/web-app/src/queries/index.js
|
||||
waspComplexTest/.wasp/out/web-app/src/queryClient.js
|
||||
waspComplexTest/.wasp/out/web-app/src/router.jsx
|
||||
waspComplexTest/.wasp/out/web-app/src/universal/url.ts
|
||||
waspComplexTest/.wasp/out/web-app/src/utils.js
|
||||
waspComplexTest/.wasp/out/web-app/src/vite-env.d.ts
|
||||
waspComplexTest/.wasp/out/web-app/tsconfig.json
|
||||
waspComplexTest/.wasp/out/web-app/tsconfig.node.json
|
||||
waspComplexTest/.wasp/out/web-app/vite.config.ts
|
||||
waspComplexTest/.wasproot
|
||||
waspComplexTest/main.wasp
|
||||
waspComplexTest/src/.waspignore
|
||||
waspComplexTest/src/client/App.jsx
|
||||
waspComplexTest/src/client/Main.css
|
||||
waspComplexTest/src/client/MainPage.jsx
|
||||
waspComplexTest/src/client/myClientSetupCode.js
|
||||
waspComplexTest/src/client/tsconfig.json
|
||||
waspComplexTest/src/client/vite-env.d.ts
|
||||
waspComplexTest/src/client/waspLogo.png
|
||||
waspComplexTest/src/server/actions/bar.js
|
||||
waspComplexTest/src/server/jobs/bar.js
|
||||
waspComplexTest/src/server/myServerSetupCode.js
|
||||
waspComplexTest/src/server/queries/bar.js
|
||||
waspComplexTest/src/server/tsconfig.json
|
||||
waspComplexTest/src/shared/tsconfig.json
|
3
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.env.server
generated
Normal file
3
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.env.server
generated
Normal file
@ -0,0 +1,3 @@
|
||||
GOOGLE_CLIENT_ID=google_client_id
|
||||
GOOGLE_CLIENT_SECRET=google_client_secret
|
||||
|
2
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.dockerignore
generated
Normal file
2
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.dockerignore
generated
Normal file
@ -0,0 +1,2 @@
|
||||
**/node_modules/
|
||||
**/.git
|
758
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums
generated
Normal file
758
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums
generated
Normal file
@ -0,0 +1,758 @@
|
||||
[
|
||||
[
|
||||
[
|
||||
"file",
|
||||
".dockerignore"
|
||||
],
|
||||
"a2a5ebe3785244437c0d490b4bfa1652c84b9ab0d4f25566fd7e69d1421d7a1b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"Dockerfile"
|
||||
],
|
||||
"276762c9c450a937b3fe64f660877e60b82ee12d5ef41cef894037dc0784ade5"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"db/schema.prisma"
|
||||
],
|
||||
"ca888a6ed14f8ad3980e4aa1c35d88a7268d8936134f78efc5a19add7abe9fc0"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/.env"
|
||||
],
|
||||
"7868f4dd9dc579bb21167f4e4cd5caa2960bf44ebe7d3f9f27a2f9380491d31f"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/.gitignore"
|
||||
],
|
||||
"2c3d81dd719ff91d51609a02d03ba62ad456e2c50763b7b4c761395cb583f890"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/.npmrc"
|
||||
],
|
||||
"8a74b1091affea3c71c2cf04acbaa38c4a51a261a9a98dcc9e96473d07a85a27"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/README.md"
|
||||
],
|
||||
"e14514b8b152dd2160a217d8fc66a710c8607c4ed1441a2148a0770701818249"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/nodemon.json"
|
||||
],
|
||||
"c4c69994805c8e0d993bd39fec9e52478b5e656bae565fa1c19ef378901a7139"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/package.json"
|
||||
],
|
||||
"45dd065b8f3937ad56587759a30dc25fc467783a8f9cdd6e68f15fbed5babdff"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/patches/oauth+0.9.15.patch"
|
||||
],
|
||||
"8b19a9638456f4f111d1fe95c90e9b0843773abaae62a356e3530609072560d0"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/scripts/universal/validators.mjs"
|
||||
],
|
||||
"94edf3906c43fb476b22244d07a5d69fd6c967de767532ed5b3215e2a6eee91b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/scripts/validate-env.mjs"
|
||||
],
|
||||
"b507ff0f8cbdac18a0b42ccbb7acda9a5cd88e0ac5f95ca3d42341944298062d"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/_types/index.ts"
|
||||
],
|
||||
"64552c6b9e03c30d670161bd3912cd8050b9be8255f76a024c0995b9a38b5f55"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/_types/taggedEntities.ts"
|
||||
],
|
||||
"ac5570e21d89fbb8418a2fc26cbbec7b189c13b4bfab5f0b006d356411b8f5a2"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/actions/MySpecialAction.js"
|
||||
],
|
||||
"2dce5f6e711a9b7d94ce57c17ec27c5f86149baaf4a2ad9bc4a108756b13145e"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/actions/types.ts"
|
||||
],
|
||||
"225c35a21bce64ec2081013c7aadef5bb7ebbc3bdad617d08390164e150555aa"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/app.js"
|
||||
],
|
||||
"1e802078a0c6738f9dc2dc8f1739120d28fdc3d6fdc8029671ec9aed73c8ed72"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/config.js"
|
||||
],
|
||||
"99e89fb4d207108caf0afeaf8f364819bac6f6d7c28a0a14b6ae7a4f134aa779"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/core/AuthError.js"
|
||||
],
|
||||
"5ac18ed20c5c8b192a75d1cb2f36906d5ff9f0951451a2ef08d9ab62fc26b8bb"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/core/HttpError.js"
|
||||
],
|
||||
"fa06ff60ab8183e8d00c13805692fc8cd3b5a21408edce4f0949d5b913e066b1"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/core/auth.js"
|
||||
],
|
||||
"968abe76a1cf4627b2bce2b9504558dbb1a8e5a50690262d6d4cd9506066ee72"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/core/auth/prismaMiddleware.js"
|
||||
],
|
||||
"aede440c2297aa5028e5f387e3392228d8249fb4906aeb95364006aeec039aae"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/dbClient.js"
|
||||
],
|
||||
"5fb53eff5e5eae318e281a8fc1cc433b69688d93c3c82d72415f710a6e75e2af"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/entities/index.ts"
|
||||
],
|
||||
"3dc318116ab03fc779f408442a2608011ea1f3d8f9a28109f27041b7d22ef3d1"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/ext-src/actions/bar.js"
|
||||
],
|
||||
"83c606a3eee7608155cdb2c2a20a38f851a82987e060ce25b196b467092c4740"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/ext-src/jobs/bar.js"
|
||||
],
|
||||
"83c606a3eee7608155cdb2c2a20a38f851a82987e060ce25b196b467092c4740"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/ext-src/myServerSetupCode.js"
|
||||
],
|
||||
"a5819016c8dd9e370e9ce89cd636a33c6eafa55f8fa38f31c3af453526f77cc2"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/ext-src/queries/bar.js"
|
||||
],
|
||||
"83c606a3eee7608155cdb2c2a20a38f851a82987e060ce25b196b467092c4740"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/jobs/MySpecialJob.js"
|
||||
],
|
||||
"9bf6a5f7005d3ab4ca933fb239ef21f13ba68aef30d8767230d6cc03911ca0e1"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/jobs/core/Job.js"
|
||||
],
|
||||
"e0e5d5e802a29032bfc8426097950722ac0dc7931d08641c1c2b02c262e6cdcc"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/jobs/core/SubmittedJob.js"
|
||||
],
|
||||
"75753277b6bd2c1d2e9ea0e80a71c72c84fa18bb7d61da25d798b3ef247e06bd"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/jobs/core/allJobs.js"
|
||||
],
|
||||
"b5ddc268dfe8f1f7d96d775c1d8d407a647a45ed4937a87da7eb1eb50f9a4674"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/jobs/core/pgBoss/pgBoss.js"
|
||||
],
|
||||
"9821963d90b39058285343834c70e6f825d3d7696c738fd95539614b5e7d7b94"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/jobs/core/pgBoss/pgBossJob.js"
|
||||
],
|
||||
"ff6040d051c916eb080a2f2c37fd5135f588782387faeae51115d1a7abd1ad8b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/jobs/core/simpleJob.js"
|
||||
],
|
||||
"36fe173d9f5128859196bfd3a661983df2d95eb34d165a469b840982b06cf59b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/queries/MySpecialQuery.js"
|
||||
],
|
||||
"ecb3145ccbb1b4888db63b5cb000f4d65721d5b6ea99cc670d9f8faaddf0c51d"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/queries/types.ts"
|
||||
],
|
||||
"ddd91bb1d15b1c3e684726a6ea56d02c4bd103654b8c37373b943f64c12c0e29"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/index.js"
|
||||
],
|
||||
"ac6c9f57907b95529955248014d4a0b87d9883afba9f725e8bb6da7e9cdcbaef"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/login.js"
|
||||
],
|
||||
"0a5d88e1a91ceb6a893e286ed2e1167bf418ca9d226341d6ee6dc6c0fac54318"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/me.js"
|
||||
],
|
||||
"65ac0f6fe12e13ffeb67b01b62301c4c70e12707985bfa567aea0253649e9151"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/passport/generic/provider.js"
|
||||
],
|
||||
"bac53b5bb71b55593c97c18dcb26fe05c9fa3487dd7a63ee64940362100853ef"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/passport/google/config.js"
|
||||
],
|
||||
"40b271cec4039bcd0435e1766b0fdf6954caff6401db1272034848ea5bef97b3"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/passport/google/configMapping.js"
|
||||
],
|
||||
"bb20100bf23ee29e783f0d486f4b3fc75792742f4b92f9547c4b12216afebbb7"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/passport/google/defaults.js"
|
||||
],
|
||||
"e390f515ed581c51b75b43e716e346710c17fb1e99470f944de22fc22925a2e1"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/passport/passport.js"
|
||||
],
|
||||
"8798e7255db79cd528776cc9c72781950f69314277c6eff8b5320e6ffa722a2e"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/signup.js"
|
||||
],
|
||||
"7e1975a564b8d450f6c411753bfe4ab45d76b209bb28cfc86eb514236a018219"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/utils.js"
|
||||
],
|
||||
"c2752f5686d06db30aaefe7e7d2bae5e5d988bf9f34e04c262b73a69179521ae"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/index.js"
|
||||
],
|
||||
"bd024209f256910dc8450851bf64d5309423e04f34bc5941cfb692df7907aa27"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/operations/MySpecialAction.js"
|
||||
],
|
||||
"a811906270902af1259bffee936c9628e5aa3235cf9b08cc52b1acd54adebb2c"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/operations/MySpecialQuery.js"
|
||||
],
|
||||
"5e4ef25a29bff0806cb2abef2e9ff8314232a5e505dbec0b9ace56d1c3369237"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/operations/index.js"
|
||||
],
|
||||
"0cb1f33b2ddb9dbe6bb2640f899aa151b388e1799e97b28390af6404154f28c0"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/server.ts"
|
||||
],
|
||||
"e27f5f548ccf994e7a60c8e28b2eec80bb578d0da5c19cadb83b45745bc6fb84"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/types/index.ts"
|
||||
],
|
||||
"37352cdaeae444c90632fe788d1e49eca3927ef8755702ba95c81c8b90d9f408"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/universal/url.ts"
|
||||
],
|
||||
"8dc6e044a1a231b796465d94985ca47c5efd42a5d411b407a7d83a61ebae4b6d"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/utils.js"
|
||||
],
|
||||
"68a5794f55e24b303d81456a1181a3a2cd70773f6ebc4e7a63dac064834aa8e9"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/tsconfig.json"
|
||||
],
|
||||
"c58318dce36bc48fdd1e998af906b5026c87b1d80415653b03a7285acdb56d69"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/.gitignore"
|
||||
],
|
||||
"d373417ea2a8844e3b7a1515aab123e90fbeb4b7225144d55d243d9f6f04f73a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/.npmrc"
|
||||
],
|
||||
"7151cf397def0c2cb0ab65643701d27d335a72c90f775675b5f826bc7005818a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/README.md"
|
||||
],
|
||||
"aa1bd54732b015158d24c55e03bc79b2f211109bc2b0478d6f2ad6b52d15847f"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/index.html"
|
||||
],
|
||||
"f1ca90861003c568c2670b0064f15431cfc45a68b23ffd890b1187af588a76d9"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/netlify.toml"
|
||||
],
|
||||
"854009c7d1b2630a55099439f89e4947188ea4160c9d1388b46b5a5cf6ab461a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/package.json"
|
||||
],
|
||||
"7076213d4eff673d37d271baa15cdda5cd5a647d31944015f52ef72aad558ac8"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/public/favicon.ico"
|
||||
],
|
||||
"1481f03584b46b63182c1f8d26e61a95ce4d8fae032b2b4f3fe5e00ab7e96c23"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/public/images/google-logo-icon.png"
|
||||
],
|
||||
"e2087f585c3b213ba537a56c8bc8e6134c69d6fa1a5728d306df56d697b4e7ab"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/public/manifest.json"
|
||||
],
|
||||
"b57981d1636058192ae6057166991125031f519f24c0a0711ef16dc73958e51a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/scripts/universal/validators.mjs"
|
||||
],
|
||||
"94edf3906c43fb476b22244d07a5d69fd6c967de767532ed5b3215e2a6eee91b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/scripts/validate-env.mjs"
|
||||
],
|
||||
"690460410bc9b28a8e96c6e20902e841f07b77faccf3db7e7249f48f700f4bab"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/actions/MySpecialAction.js"
|
||||
],
|
||||
"55e4031ed91e5dd8c1aaa5cf64e3295356938966f167c6950d81c9b7a77db787"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/actions/core.d.ts"
|
||||
],
|
||||
"d649b4823ede60119858b868dac9b122da4a3185391e6b9242ff7f6dc5c25d31"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/actions/core.js"
|
||||
],
|
||||
"5c4dcdec74fb014a8edbb3d240bcbbfc829e201bce64132598b444db14a2bd45"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"0f294c2f1d50a1473f6d332ef17944e7475c7d83a2180f6e2c9c9aecf25439f4"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/api.js"
|
||||
],
|
||||
"bcdb1fdc97fd622fbc882e4a16a7137e849748d6776b960121df6ccab577a3a9"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/Login.jsx"
|
||||
],
|
||||
"2a97bf6cb1ba5cd1f271dc83aaac5864a7e5738b72814c7d09bb3e5f70877686"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/Signup.jsx"
|
||||
],
|
||||
"712a1513116da68cef5a8dddcf80fbaffa8b733690c52e7c5cc1ef329af591e9"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/helpers/Google.jsx"
|
||||
],
|
||||
"7707679f4ef70592429401cf932206b5cfd87dd80d244e3e8c05803658406228"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/helpers/user.ts"
|
||||
],
|
||||
"7ae0fa01ac15b43746877b765bc4e6fd533f302c435b771238ad79afbe923000"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/login.js"
|
||||
],
|
||||
"69b575be2d52e2948983f82c2d3fdabcc9d5fad9fd54c21252e9e7daa0edab32"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/logout.js"
|
||||
],
|
||||
"041fe67384d24bf43a61b6ad9160b68309d8d1f5f3dd34f1961c93585d4eff50"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/pages/OAuthCodeExchange.jsx"
|
||||
],
|
||||
"bb202daa3b41c8288252d56a74ed78bed7129d7c96be96171cd516795d0ff71b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/pages/createAuthRequiredPage.jsx"
|
||||
],
|
||||
"796720e403436139720fa72b941bc5fcf190b05777693c62d2b5f638e872badc"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/signup.js"
|
||||
],
|
||||
"e2943a925604b86cb1eba6c8d5a99c6892d8d3b55940c656233aa1163a622a6d"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/useAuth.js"
|
||||
],
|
||||
"6ab962413a3103fa4e2cb8f7d4751ee93da484f9312c007e0b4d28bfa130a367"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/config.js"
|
||||
],
|
||||
"b06175129dd8be8ca6c307fafa02646d47233c4e26fdfeea6d7802d02e9513f3"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/entities/index.ts"
|
||||
],
|
||||
"642ab14a4d5b92152ba458b210502adea70fa7a6d1d6af35108a71236a33e89b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/ext-src/App.jsx"
|
||||
],
|
||||
"b23058c8652ddb88b8e147e20fb97c7e90c076b5224314f6a3ab4c620957c0ae"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/ext-src/Main.css"
|
||||
],
|
||||
"1583ce49887ea575e563dab0c7ad6d2e2e3e6a351ed7c2f80c310641b1456417"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/ext-src/MainPage.jsx"
|
||||
],
|
||||
"7244c106359f088fdcc0d4a76ee63277f1cea63cbe7aac5e5c39a17df693b1e2"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/ext-src/myClientSetupCode.js"
|
||||
],
|
||||
"32219bbb2a8d08dfa8447255b922c9d0d202d21a38e2fbc281b62b181b6f5134"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/ext-src/vite-env.d.ts"
|
||||
],
|
||||
"d677883c270ee71acc9bfd95c824650b541036599427a8ffc0ad33323d08e61d"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/ext-src/waspLogo.png"
|
||||
],
|
||||
"0f05a89eb945d6d7326110e88776e402833b356202b06d0a8bf652e118d3fd2f"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/index.tsx"
|
||||
],
|
||||
"bcb8202d7b6c536d1589aa5ec64407e6e856fd4b3866ec8e2111389fee2c751d"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/logo.png"
|
||||
],
|
||||
"35f14abf46cb0e9d9b4e3d11961917eecb86be65a0d7f4e6bc9713c5766b5f1b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/operations/index.js"
|
||||
],
|
||||
"6ab717db2304b6134073aa71144b213b86f8d68a106549da06e193d18683dd87"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/operations/resources.js"
|
||||
],
|
||||
"8bb71b7d7ef3962c28b9db1554500452e7f91b0c9fd898d5cafcdb7229f6db1e"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/operations/updateHandlersMap.js"
|
||||
],
|
||||
"07a9933a771853585fd2fc49c3dcb9f1cad947acb2168580c835fae25620fa9f"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/queries/MySpecialQuery.js"
|
||||
],
|
||||
"277428b1579f92eb2fb04b323fa7b9c0acaad935d4f3a451c72694aee8605a32"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/queries/core.d.ts"
|
||||
],
|
||||
"f0b289140e92738451be386ca73a2fd1c84e9951eb2f1b9c6c09dfa3079d0c74"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/queries/core.js"
|
||||
],
|
||||
"2daf5b414722204281d65e954ce862a6fc586e8907b202800694909d23957c5e"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/queries/index.d.ts"
|
||||
],
|
||||
"efc70de9916a60e19e0c86aaf955b0be0c999ba5c30139c3b6b98bcc4d382091"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/queries/index.js"
|
||||
],
|
||||
"8379699fa710f43d5be2b96f3a5c7c551a8d3d9db3ef0fea964502da00f36ffc"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/queryClient.js"
|
||||
],
|
||||
"1739d8618286b4a2aa536bd3fcb754514c828ec896b69c7074e22f66c07e7b03"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/router.jsx"
|
||||
],
|
||||
"a92e5d20f1e2bbb7b4cbfae12e145338ddf34c38823c4b49c954da3c9b623157"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/universal/url.ts"
|
||||
],
|
||||
"8dc6e044a1a231b796465d94985ca47c5efd42a5d411b407a7d83a61ebae4b6d"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/utils.js"
|
||||
],
|
||||
"0ae6e53a050dbc6b7166aedfd5b076c2cab524c4d6aff884dc6902b178148815"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/vite-env.d.ts"
|
||||
],
|
||||
"65996936fbb042915f7b74a200fcdde7e410f32a669b1ab9597cfaa4b0faddb5"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/tsconfig.json"
|
||||
],
|
||||
"49258557f92e10eddda507885655d22f51d52f6f39e277c2ec1c9d626018c728"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/tsconfig.node.json"
|
||||
],
|
||||
"c40d0614fda5b8a746a1ba2e3be8e2370140c0167851ac77881cd53bb5647cdd"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/vite.config.ts"
|
||||
],
|
||||
"ab5c138ab0ab01f5108c2da9a8bf6b673b79115ca97276f6293da8e3bc0a45b1"
|
||||
]
|
||||
]
|
54
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/Dockerfile
generated
Normal file
54
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/Dockerfile
generated
Normal file
@ -0,0 +1,54 @@
|
||||
# NOTE: Why do we specify alpine version here?
|
||||
# Because if not, we had situations where it would use the different version
|
||||
# locally and on Github CI. This way we ensure exact version is used,
|
||||
# and also have control over updating it (instead of update surprising us).
|
||||
FROM node:18-alpine3.17 AS node
|
||||
|
||||
|
||||
# We split Dockerfile into base, server-builder and server-production.
|
||||
# This way we have separate situations -> in server-builder we build all
|
||||
# we need to run the server, and then in server-production we start fresh
|
||||
# and just copy what we need from server-builder, avoiding intermediate
|
||||
# artifacts and any settings / pollution we don't need in production
|
||||
# but only for building.
|
||||
|
||||
|
||||
FROM node AS base
|
||||
RUN apk --no-cache -U upgrade # To ensure any potential security patches are applied.
|
||||
# TODO: Remove line below (installation of openssl 1.1) once Prisma adds support for
|
||||
# openssl 3 on alpine. Alpine >= 3.17 has openssl 3 as default.
|
||||
# Relevant GH issue: https://github.com/wasp-lang/wasp/issues/877
|
||||
RUN apk add --no-cache openssl1.1-compat
|
||||
|
||||
|
||||
FROM base AS server-builder
|
||||
RUN apk add --no-cache build-base libtool autoconf automake
|
||||
WORKDIR /app
|
||||
COPY server/ ./server/
|
||||
# Install npm packages, resulting in node_modules/.
|
||||
RUN cd server && npm install
|
||||
COPY db/schema.prisma ./db/
|
||||
RUN cd server && PRISMA_CLIENT_OUTPUT_DIR=../server/node_modules/.prisma/client/ npx prisma generate --schema='../db/schema.prisma'
|
||||
# Building the server should come after Prisma generation.
|
||||
RUN cd server && npm run build
|
||||
|
||||
|
||||
# TODO: Use pm2?
|
||||
# TODO: Use non-root user (node).
|
||||
FROM base AS server-production
|
||||
# In case they want to use python3 in their app.
|
||||
RUN apk add --no-cache python3
|
||||
ENV NODE_ENV production
|
||||
WORKDIR /app
|
||||
COPY --from=server-builder /app/server/node_modules ./server/node_modules
|
||||
COPY --from=server-builder /app/server/dist ./server/dist
|
||||
COPY --from=server-builder /app/server/package*.json ./server/
|
||||
COPY --from=server-builder /app/server/scripts ./server/scripts
|
||||
COPY db/ ./db/
|
||||
EXPOSE ${PORT}
|
||||
WORKDIR /app/server
|
||||
ENTRYPOINT ["npm", "run", "start-production"]
|
||||
|
||||
|
||||
# Any user-defined Dockerfile contents will be appended below.
|
||||
|
8
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/package.json
generated
Normal file
8
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/package.json
generated
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@prisma/client": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^4.5.0"
|
||||
}
|
||||
}
|
28
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma
generated
Normal file
28
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma
generated
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = env("PRISMA_CLIENT_OUTPUT_DIR")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
externalAuthAssociations SocialLogin[]
|
||||
|
||||
}
|
||||
model SocialLogin {
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
ca888a6ed14f8ad3980e4aa1c35d88a7268d8936134f78efc5a19add7abe9fc0
|
@ -0,0 +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.merge","version":"^4.6.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.0.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"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/express","version":"^4.17.13"},{"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":"@prisma/client","version":"4.5.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"}]}}
|
3
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/.env
generated
Normal file
3
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/.env
generated
Normal file
@ -0,0 +1,3 @@
|
||||
GOOGLE_CLIENT_ID=google_client_id
|
||||
GOOGLE_CLIENT_SECRET=google_client_secret
|
||||
|
1
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/.npmrc
generated
Normal file
1
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/.npmrc
generated
Normal file
@ -0,0 +1 @@
|
||||
engine-strict=true
|
5
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/README.md
generated
Normal file
5
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/README.md
generated
Normal file
@ -0,0 +1,5 @@
|
||||
`npm start` to run server in development mode (it reloads on changes).
|
||||
|
||||
`npm run debug` to run `npm start` with debug logs enabled.
|
||||
|
||||
`npm run standard` to run StandardJS.
|
10
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/nodemon.json
generated
Normal file
10
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/nodemon.json
generated
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"watch": [
|
||||
"src/",
|
||||
".env"
|
||||
],
|
||||
"ext": "ts,mts,js,mjs,json"
|
||||
}
|
58
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/package.json
generated
Normal file
58
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/package.json
generated
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@prisma/client": "4.5.0",
|
||||
"cookie-parser": "~1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "16.0.2",
|
||||
"express": "~4.18.1",
|
||||
"helmet": "^6.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"morgan": "~1.10.0",
|
||||
"passport": "0.6.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"patch-package": "^6.4.7",
|
||||
"pg-boss": "^8.0.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"redux": "^4.0.5",
|
||||
"secure-password": "^4.0.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node18": "^1.0.1",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/node": "^18.11.9",
|
||||
"nodemon": "^2.0.19",
|
||||
"prisma": "4.5.0",
|
||||
"standard": "^17.0.0",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"engines": {
|
||||
"node": "^18.12.0"
|
||||
},
|
||||
"name": "server",
|
||||
"nodemonConfig": {
|
||||
"delay": "1000"
|
||||
},
|
||||
"overrides": {
|
||||
"secure-password": {
|
||||
"sodium-native": "3.3.0"
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npx tsc",
|
||||
"build-and-start": "npm run build && npm run start",
|
||||
"db-migrate-dev": "prisma migrate dev --schema=../db/schema.prisma",
|
||||
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
|
||||
"postinstall": "patch-package",
|
||||
"standard": "standard",
|
||||
"start": "npm run validate-env && NODE_PATH=dist node -r dotenv/config dist/server.js",
|
||||
"start-production": "npm run db-migrate-prod && NODE_ENV=production npm run start",
|
||||
"validate-env": "node -r dotenv/config ./scripts/validate-env.mjs",
|
||||
"watch": "nodemon --exec 'npm run build-and-start || exit 1'"
|
||||
},
|
||||
"type": "module",
|
||||
"version": "0.0.0"
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
diff --git a/node_modules/oauth/lib/oauth2.js b/node_modules/oauth/lib/oauth2.js
|
||||
index 77241c4..b6c0184 100644
|
||||
--- a/node_modules/oauth/lib/oauth2.js
|
||||
+++ b/node_modules/oauth/lib/oauth2.js
|
||||
@@ -158,8 +158,14 @@ exports.OAuth2.prototype._executeRequest= function( http_library, options, post_
|
||||
});
|
||||
});
|
||||
request.on('error', function(e) {
|
||||
- callbackCalled= true;
|
||||
- callback(e);
|
||||
+ // Ref: https://github.com/ciaranj/node-oauth/pull/363
|
||||
+ // `www.googleapis.com` does `ECONNRESET` just after data is received in `passBackControl`
|
||||
+ // this prevents the callback from being called twice, first in passBackControl and second time in here
|
||||
+ // see also NodeJS Stream documentation: "The 'error' event may be emitted by a Readable implementation at any time"
|
||||
+ if(!callbackCalled) {
|
||||
+ callbackCalled= true;
|
||||
+ callback(e);
|
||||
+ }
|
||||
});
|
||||
|
||||
if( (options.method == 'POST' || options.method == 'PUT') && post_body ) {
|
@ -0,0 +1,21 @@
|
||||
export function isValidAbsoluteURL(rawUrl) {
|
||||
try {
|
||||
const url = new URL(rawUrl);
|
||||
/*
|
||||
URL constructor will parse some invalid absolute URLs as valid URLs
|
||||
so we need to do some extra checks.
|
||||
|
||||
Example: "localhost:3000" will be parsed as URL with protocol of
|
||||
"localhost:" and host of "3000"
|
||||
*/
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function throwIfNotValidAbsoluteURL(value, name) {
|
||||
if (value && !isValidAbsoluteURL(value)) {
|
||||
throw new Error(`${name} must be a valid absolute URL`);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import { throwIfNotValidAbsoluteURL } from './universal/validators.mjs';
|
||||
|
||||
console.info("🔍 Validating environment variables...");
|
||||
throwIfNotValidAbsoluteURL(process.env.WASP_WEB_CLIENT_URL, 'Environment variable WASP_WEB_CLIENT_URL');
|
@ -0,0 +1,51 @@
|
||||
import prisma from "../dbClient.js"
|
||||
import { type User } from "../entities"
|
||||
import { type _Entity } from "./taggedEntities"
|
||||
|
||||
export * from "./taggedEntities"
|
||||
|
||||
export type Query<Entities extends _Entity[], Input, Output> = Operation<Entities, Input, Output>
|
||||
|
||||
export type Action<Entities extends _Entity[], Input, Output> = Operation<Entities, Input, Output>
|
||||
|
||||
export type AuthenticatedQuery<Entities extends _Entity[], Input, Output> =
|
||||
AuthenticatedOperation<Entities, Input, Output>
|
||||
|
||||
export type AuthenticatedAction<Entities extends _Entity[], Input, Output> =
|
||||
AuthenticatedOperation<Entities, Input, Output>
|
||||
|
||||
type AuthenticatedOperation<Entities extends _Entity[], Input, Output> = (
|
||||
args: Input,
|
||||
context: Expand<OperationContext<Entities> & {
|
||||
// TODO: This type must match the logic in core/auth.js (if we remove the
|
||||
// password field from the object there, we must do the same here). Ideally,
|
||||
// these two things would live in the same place:
|
||||
// https://github.com/wasp-lang/wasp/issues/965
|
||||
user: Omit<User, 'password'>
|
||||
}>,
|
||||
) => Promise<Output>
|
||||
|
||||
type Operation<Entities extends _Entity[], Input, Output> = (
|
||||
args: Input,
|
||||
context: Expand<OperationContext<Entities>>,
|
||||
) => Promise<Output>
|
||||
|
||||
type OperationContext<Entities extends _Entity[]> = {
|
||||
entities: Expand<EntityMap<Entities>>
|
||||
}
|
||||
|
||||
type EntityMap<Entities extends _Entity[]> = {
|
||||
[EntityName in Entities[number]["_entityName"]]: PrismaDelegate[EntityName]
|
||||
}
|
||||
|
||||
type PrismaDelegate = {
|
||||
"User": typeof prisma.user,
|
||||
"SocialLogin": typeof prisma.socialLogin,
|
||||
}
|
||||
|
||||
// This is a helper type used exclusively for DX purposes. It's a No-op for the
|
||||
// compiler, but expands the type's representatoin in IDEs (i.e., inlines all
|
||||
// type constructors) to make it more readable for the user.
|
||||
//
|
||||
// Check this SO answer for details: https://stackoverflow.com/a/57683652
|
||||
type Expand<T extends object> = T extends infer O ? { [K in keyof O]: O[K] } : never
|
@ -0,0 +1,22 @@
|
||||
// Wasp internally uses the types defined in this file for typing entity maps in
|
||||
// operation contexts.
|
||||
//
|
||||
// We must explicitly tag all entities with their name to avoid issues with
|
||||
// structural typing. See https://github.com/wasp-lang/wasp/pull/982 for details.
|
||||
import {
|
||||
type Entity,
|
||||
type EntityName,
|
||||
type User,
|
||||
type SocialLogin,
|
||||
} from '../entities'
|
||||
|
||||
export type _User = WithName<User, "User">
|
||||
export type _SocialLogin = WithName<SocialLogin, "SocialLogin">
|
||||
|
||||
export type _Entity =
|
||||
| _User
|
||||
| _SocialLogin
|
||||
| never
|
||||
|
||||
type WithName<E extends Entity, Name extends EntityName> =
|
||||
E & { _entityName: Name }
|
@ -0,0 +1,13 @@
|
||||
import prisma from '../dbClient.js'
|
||||
|
||||
import { foo } from '../ext-src/actions/bar.js'
|
||||
|
||||
|
||||
export default async function (args, context) {
|
||||
return foo(args, {
|
||||
...context,
|
||||
entities: {
|
||||
User: prisma.user,
|
||||
},
|
||||
})
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import {
|
||||
type _User,
|
||||
type AuthenticatedAction,
|
||||
} from '../_types'
|
||||
|
||||
export type MySpecialAction<Input = never, Output = unknown> =
|
||||
AuthenticatedAction<
|
||||
[
|
||||
_User,
|
||||
],
|
||||
Input,
|
||||
Output
|
||||
>
|
||||
|
41
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/app.js
generated
Normal file
41
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/app.js
generated
Normal file
@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import HttpError from './core/HttpError.js'
|
||||
import indexRouter from './routes/index.js'
|
||||
import config from './config.js'
|
||||
|
||||
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
|
||||
// it can be used in unit tests to test each route individually.
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(helmet())
|
||||
app.use(cors({
|
||||
// TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
|
||||
origin: config.frontendUrl,
|
||||
}))
|
||||
app.use(logger('dev'))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
app.use(cookieParser())
|
||||
|
||||
app.use('/', indexRouter)
|
||||
|
||||
// Custom error handler.
|
||||
app.use((err, req, res, next) => {
|
||||
// As by expressjs documentation, when the headers have already
|
||||
// been sent to the client, we must delegate to the default error handler.
|
||||
if (res.headersSent) { return next(err) }
|
||||
|
||||
if (err instanceof HttpError) {
|
||||
return res.status(err.statusCode).json({ message: err.message, data: err.data })
|
||||
}
|
||||
|
||||
return next(err)
|
||||
})
|
||||
|
||||
export default app
|
37
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/config.js
generated
Normal file
37
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/config.js
generated
Normal file
@ -0,0 +1,37 @@
|
||||
import merge from 'lodash.merge'
|
||||
|
||||
import { stripTrailingSlash } from "./universal/url.js";
|
||||
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
|
||||
// TODO:
|
||||
// - Use dotenv library to consume env vars from a file.
|
||||
// - Use convict library to define schema and validate env vars.
|
||||
// https://codingsans.com/blog/node-config-best-practices
|
||||
|
||||
const config = {
|
||||
all: {
|
||||
env,
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
databaseUrl: process.env.DATABASE_URL,
|
||||
frontendUrl: undefined,
|
||||
auth: {
|
||||
jwtSecret: undefined
|
||||
}
|
||||
},
|
||||
development: {
|
||||
frontendUrl: stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL) || 'http://localhost:3000',
|
||||
auth: {
|
||||
jwtSecret: 'DEVJWTSECRET'
|
||||
}
|
||||
},
|
||||
production: {
|
||||
frontendUrl: stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL),
|
||||
auth: {
|
||||
jwtSecret: process.env.JWT_SECRET
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedConfig = merge(config.all, config[env])
|
||||
export default resolvedConfig
|
@ -0,0 +1,17 @@
|
||||
class AuthError extends Error {
|
||||
constructor (message, data, ...params) {
|
||||
super(message, ...params)
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, AuthError)
|
||||
}
|
||||
|
||||
this.name = this.constructor.name
|
||||
|
||||
if (data) {
|
||||
this.data = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthError
|
@ -0,0 +1,22 @@
|
||||
class HttpError extends Error {
|
||||
constructor (statusCode, message, data, ...params) {
|
||||
super(message, ...params)
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, HttpError)
|
||||
}
|
||||
|
||||
this.name = this.constructor.name
|
||||
|
||||
if (!(Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600)) {
|
||||
throw new Error('statusCode has to be integer in range [400, 600).')
|
||||
}
|
||||
this.statusCode = statusCode
|
||||
|
||||
if (data) {
|
||||
this.data = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default HttpError
|
124
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js
generated
Normal file
124
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js
generated
Normal file
@ -0,0 +1,124 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import SecurePassword from 'secure-password'
|
||||
import util from 'util'
|
||||
import { randomInt } from 'node:crypto'
|
||||
|
||||
import prisma from '../dbClient.js'
|
||||
import { handleRejection } from '../utils.js'
|
||||
import config from '../config.js'
|
||||
|
||||
const jwtSign = util.promisify(jwt.sign)
|
||||
const jwtVerify = util.promisify(jwt.verify)
|
||||
|
||||
const JWT_SECRET = config.auth.jwtSecret
|
||||
|
||||
export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options)
|
||||
export const verify = (token) => jwtVerify(token, JWT_SECRET)
|
||||
|
||||
const auth = handleRejection(async (req, res, next) => {
|
||||
const authHeader = req.get('Authorization')
|
||||
if (!authHeader) {
|
||||
// NOTE(matija): for now we let tokenless requests through and make it operation's
|
||||
// responsibility to verify whether the request is authenticated or not. In the future
|
||||
// we will develop our own system at Wasp-level for that.
|
||||
return next()
|
||||
}
|
||||
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7, authHeader.length)
|
||||
|
||||
let userIdFromToken
|
||||
try {
|
||||
userIdFromToken = (await verify(token)).id
|
||||
} catch (error) {
|
||||
if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) {
|
||||
return res.status(401).send()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: userIdFromToken } })
|
||||
if (!user) {
|
||||
return res.status(401).send()
|
||||
}
|
||||
|
||||
// TODO: This logic must match the type in types/index.ts (if we remove the
|
||||
// password field from the object here, we must to do the same there).
|
||||
// Ideally, these two things would live in the same place:
|
||||
// https://github.com/wasp-lang/wasp/issues/965
|
||||
const { password, ...userView } = user
|
||||
|
||||
req.user = userView
|
||||
} else {
|
||||
return res.status(401).send()
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
const SP = new SecurePassword()
|
||||
|
||||
export const hashPassword = async (password) => {
|
||||
const hashedPwdBuffer = await SP.hash(Buffer.from(password))
|
||||
return hashedPwdBuffer.toString("base64")
|
||||
}
|
||||
|
||||
export const verifyPassword = async (hashedPassword, password) => {
|
||||
try {
|
||||
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Generates an unused username that looks similar to "quick-purple-sheep-91231".
|
||||
// It generates several options and ensures it picks one that is not currently in use.
|
||||
export function generateAvailableDictionaryUsername() {
|
||||
const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young']
|
||||
const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow']
|
||||
const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep']
|
||||
|
||||
const potentialUsernames = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}`
|
||||
potentialUsernames.push(potentialUsername)
|
||||
}
|
||||
|
||||
return findAvailableUsername(potentialUsernames)
|
||||
}
|
||||
|
||||
// Generates an unused username based on an array of username segments and a separator.
|
||||
// It generates several options and ensures it picks one that is not currently in use.
|
||||
export function generateAvailableUsername(usernameSegments, config) {
|
||||
const separator = config?.separator || '-'
|
||||
const baseUsername = usernameSegments.join(separator)
|
||||
|
||||
const potentialUsernames = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}`
|
||||
potentialUsernames.push(potentialUsername)
|
||||
}
|
||||
|
||||
return findAvailableUsername(potentialUsernames)
|
||||
}
|
||||
|
||||
// Checks the database for an unused username from an array provided and returns first.
|
||||
async function findAvailableUsername(potentialUsernames) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: { in: potentialUsernames },
|
||||
}
|
||||
})
|
||||
const takenUsernames = users.map(user => user.username)
|
||||
const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username))
|
||||
|
||||
if (availableUsernames.length === 0) {
|
||||
throw new Error('Unable to generate a unique username. Please contact Wasp.')
|
||||
}
|
||||
|
||||
return availableUsernames[0]
|
||||
}
|
||||
|
||||
export default auth
|
@ -0,0 +1,85 @@
|
||||
import { hashPassword } from '../auth.js'
|
||||
import AuthError from '../AuthError.js'
|
||||
|
||||
const USERNAME_FIELD = 'username'
|
||||
const PASSWORD_FIELD = 'password'
|
||||
|
||||
// Allows flexible validation of a user entity.
|
||||
// Users can skip default validations by passing _waspSkipDefaultValidations = true
|
||||
// Users can also add custom validations by passing an array of _waspCustomValidations
|
||||
// with the same format as our default validations.
|
||||
// Throws an AuthError on the first validation that fails.
|
||||
const registerUserEntityValidation = (prismaClient) => {
|
||||
prismaClient.$use(async (params, next) => {
|
||||
if (params.model === 'User') {
|
||||
if (['create', 'update', 'updateMany'].includes(params.action)) {
|
||||
validateUser(params.args.data, params.args, params.action)
|
||||
} else if (params.action === 'upsert') {
|
||||
validateUser(params.args.create, params.args, 'create')
|
||||
validateUser(params.args.update, params.args, 'update')
|
||||
}
|
||||
|
||||
// Remove from downstream Prisma processing to avoid "Unknown arg" error
|
||||
delete params.args._waspSkipDefaultValidations
|
||||
delete params.args._waspCustomValidations
|
||||
}
|
||||
|
||||
return next(params)
|
||||
})
|
||||
}
|
||||
|
||||
// Make sure password is always hashed before storing to the database.
|
||||
const registerPasswordHashing = (prismaClient) => {
|
||||
prismaClient.$use(async (params, next) => {
|
||||
if (params.model === 'User') {
|
||||
if (['create', 'update', 'updateMany'].includes(params.action)) {
|
||||
if (params.args.data.hasOwnProperty(PASSWORD_FIELD)) {
|
||||
params.args.data[PASSWORD_FIELD] = await hashPassword(params.args.data[PASSWORD_FIELD])
|
||||
}
|
||||
} else if (params.action === 'upsert') {
|
||||
if (params.args.create.hasOwnProperty(PASSWORD_FIELD)) {
|
||||
params.args.create[PASSWORD_FIELD] =
|
||||
await hashPassword(params.args.create[PASSWORD_FIELD])
|
||||
}
|
||||
if (params.args.update.hasOwnProperty(PASSWORD_FIELD)) {
|
||||
params.args.update[PASSWORD_FIELD] =
|
||||
await hashPassword(params.args.update[PASSWORD_FIELD])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next(params)
|
||||
})
|
||||
}
|
||||
|
||||
export const registerAuthMiddleware = (prismaClient) => {
|
||||
// NOTE: registerUserEntityValidation must come before registerPasswordHashing.
|
||||
registerUserEntityValidation(prismaClient)
|
||||
registerPasswordHashing(prismaClient)
|
||||
}
|
||||
|
||||
const validateUser = (user, args, action) => {
|
||||
user = user || {}
|
||||
|
||||
const defaultValidations = [
|
||||
{ validates: USERNAME_FIELD, message: 'username must be present', validator: username => !!username },
|
||||
{ validates: PASSWORD_FIELD, message: 'password must be present', validator: password => !!password },
|
||||
{ validates: PASSWORD_FIELD, message: 'password must be at least 8 characters', validator: password => password.length >= 8 },
|
||||
{ validates: PASSWORD_FIELD, message: 'password must contain a number', validator: password => /\d/.test(password) },
|
||||
]
|
||||
|
||||
const validations = [
|
||||
...(args._waspSkipDefaultValidations ? [] : defaultValidations),
|
||||
...(args._waspCustomValidations || [])
|
||||
]
|
||||
|
||||
// On 'create' validations run always, otherwise (on updates)
|
||||
// they run only when the field they are validating is present.
|
||||
for (const v of validations) {
|
||||
if (action === 'create' || user.hasOwnProperty(v.validates)) {
|
||||
if (!v.validator(user[v.validates])) {
|
||||
throw new AuthError(v.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/dbClient.js
generated
Normal file
19
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/dbClient.js
generated
Normal file
@ -0,0 +1,19 @@
|
||||
import Prisma from '@prisma/client'
|
||||
|
||||
|
||||
import { registerAuthMiddleware } from './core/auth/prismaMiddleware.js'
|
||||
|
||||
|
||||
const createDbClient = () => {
|
||||
const prismaClient = new Prisma.PrismaClient()
|
||||
|
||||
|
||||
registerAuthMiddleware(prismaClient)
|
||||
|
||||
|
||||
return prismaClient
|
||||
}
|
||||
|
||||
const dbClient = createDbClient()
|
||||
|
||||
export default dbClient
|
@ -0,0 +1,19 @@
|
||||
import {
|
||||
type User,
|
||||
type SocialLogin,
|
||||
} from "@prisma/client"
|
||||
|
||||
export {
|
||||
type User,
|
||||
type SocialLogin,
|
||||
} from "@prisma/client"
|
||||
|
||||
export type Entity =
|
||||
| User
|
||||
| SocialLogin
|
||||
| never
|
||||
|
||||
export type EntityName =
|
||||
| "User"
|
||||
| "SocialLogin"
|
||||
| never
|
@ -0,0 +1,4 @@
|
||||
export const foo = async (args) => {
|
||||
return 1
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
export const foo = async (args) => {
|
||||
return 1
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
export default function mySetupFunction() {
|
||||
// Do some server setup here.
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
export const foo = async (args) => {
|
||||
return 1
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
import prisma from '../dbClient.js'
|
||||
import { createJob } from './core/pgBoss/pgBossJob.js'
|
||||
import { foo } from '../ext-src/jobs/bar.js'
|
||||
|
||||
export const MySpecialJob = createJob({
|
||||
jobName: "MySpecialJob",
|
||||
jobFn: foo,
|
||||
defaultJobOptions: {},
|
||||
jobSchedule: null,
|
||||
entities: {
|
||||
},
|
||||
})
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* This is a definition of a job (think draft or invocable computation), not the running instance itself.
|
||||
* This can be submitted one or more times to be executed by some job executor via the same instance.
|
||||
* Once submitted, you get a SubmittedJob to track it later.
|
||||
*/
|
||||
export class Job {
|
||||
#jobName
|
||||
#executorName
|
||||
|
||||
/**
|
||||
* @param {string} jobName - Job name, which should be unique per executor.
|
||||
* @param {string} executorName - The name of the executor that will run submitted jobs.
|
||||
*/
|
||||
constructor(jobName, executorName) {
|
||||
this.#jobName = jobName
|
||||
this.#executorName = executorName
|
||||
}
|
||||
|
||||
get jobName() {
|
||||
return this.#jobName
|
||||
}
|
||||
|
||||
get executorName() {
|
||||
return this.#executorName
|
||||
}
|
||||
|
||||
// NOTE: Subclasses must implement this method.
|
||||
delay(...args) {
|
||||
throw new Error('Subclasses must implement this method')
|
||||
}
|
||||
|
||||
// NOTE: Subclasses must implement this method.
|
||||
async submit(...args) {
|
||||
throw new Error('Subclasses must implement this method')
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* This is the result of submitting a Job to some executor.
|
||||
* It can be used by callers to track things, or call executor-specific subclass functionality.
|
||||
*/
|
||||
export class SubmittedJob {
|
||||
#job
|
||||
#jobId
|
||||
|
||||
/**
|
||||
* @param {Job} job - The Job that submitted work to an executor.
|
||||
* @param {string} jobId - A UUID for a submitted job in that executor's ecosystem.
|
||||
*/
|
||||
constructor(job, jobId) {
|
||||
this.#job = job
|
||||
this.#jobId = jobId
|
||||
}
|
||||
|
||||
get jobId() {
|
||||
return this.#jobId
|
||||
}
|
||||
|
||||
get jobName() {
|
||||
return this.#job.jobName
|
||||
}
|
||||
|
||||
get executorName() {
|
||||
return this.#job.executorName
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
|
||||
// This module exports all jobs and is imported by the server to ensure
|
||||
// any schedules that are not referenced are still loaded by NodeJS.
|
||||
|
||||
export { MySpecialJob } from '../MySpecialJob.js'
|
@ -0,0 +1,69 @@
|
||||
import PgBoss from 'pg-boss'
|
||||
import config from '../../../config.js'
|
||||
|
||||
const boss = createPgBoss()
|
||||
|
||||
function createPgBoss() {
|
||||
let pgBossNewOptions = {
|
||||
connectionString: config.databaseUrl,
|
||||
}
|
||||
|
||||
// Add an escape hatch for advanced configuration of pg-boss to overwrite our defaults.
|
||||
if (process.env.PG_BOSS_NEW_OPTIONS) {
|
||||
try {
|
||||
pgBossNewOptions = JSON.parse(process.env.PG_BOSS_NEW_OPTIONS)
|
||||
}
|
||||
catch {
|
||||
console.error("Environment variable PG_BOSS_NEW_OPTIONS was not parsable by JSON.parse()!")
|
||||
}
|
||||
}
|
||||
|
||||
return new PgBoss(pgBossNewOptions)
|
||||
}
|
||||
|
||||
let resolvePgBossStarted, rejectPgBossStarted
|
||||
// Code that wants to access pg-boss must wait until it has been started.
|
||||
export const pgBossStarted = new Promise((resolve, reject) => {
|
||||
resolvePgBossStarted = resolve
|
||||
rejectPgBossStarted = reject
|
||||
})
|
||||
|
||||
// Ensure pg-boss can only be started once during a server's lifetime.
|
||||
const PgBossStatus = {
|
||||
Unstarted: 'Unstarted',
|
||||
Starting: 'Starting',
|
||||
Started: 'Started',
|
||||
Error: 'Error'
|
||||
}
|
||||
let pgBossStatus = PgBossStatus.Unstarted
|
||||
|
||||
/**
|
||||
* Prepares the target PostgreSQL database and begins job monitoring.
|
||||
* If the required database objects do not exist in the specified database,
|
||||
* `boss.start()` will automatically create them.
|
||||
* Ref: https://github.com/timgit/pg-boss/blob/master/docs/readme.md#start
|
||||
*
|
||||
* After making this call, we can send pg-boss jobs and they will be persisted and acted upon.
|
||||
* This should only be called once during a server's lifetime.
|
||||
*/
|
||||
export async function startPgBoss() {
|
||||
if (pgBossStatus !== PgBossStatus.Unstarted) { return }
|
||||
pgBossStatus = PgBossStatus.Starting
|
||||
console.log('Starting pg-boss...')
|
||||
|
||||
boss.on('error', error => console.error(error))
|
||||
try {
|
||||
await boss.start()
|
||||
} catch (error) {
|
||||
console.error('pg-boss failed to start!')
|
||||
console.error(error)
|
||||
pgBossStatus = PgBossStatus.Error
|
||||
rejectPgBossStarted(boss)
|
||||
return
|
||||
}
|
||||
|
||||
resolvePgBossStarted(boss)
|
||||
|
||||
console.log('pg-boss started!')
|
||||
pgBossStatus = PgBossStatus.Started
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
import { pgBossStarted } from './pgBoss.js'
|
||||
import { Job } from '../Job.js'
|
||||
import { SubmittedJob } from '../SubmittedJob.js'
|
||||
|
||||
export const PG_BOSS_EXECUTOR_NAME = Symbol('PgBoss')
|
||||
|
||||
/**
|
||||
* A pg-boss specific SubmittedJob that adds additional pg-boss functionality.
|
||||
*/
|
||||
class PgBossSubmittedJob extends SubmittedJob {
|
||||
constructor(boss, job, jobId) {
|
||||
super(job, jobId)
|
||||
this.pgBoss = {
|
||||
cancel: () => boss.cancel(jobId),
|
||||
resume: () => boss.resume(jobId),
|
||||
details: () => boss.getJobById(jobId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a class repesenting a job that can be submitted to pg-boss.
|
||||
* It is not yet submitted until the caller invokes `submit()` on an instance.
|
||||
* The caller can make as many calls to `submit()` as they wish.
|
||||
*/
|
||||
class PgBossJob extends Job {
|
||||
#defaultJobOptions
|
||||
#startAfter
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} jobName - The name of the Job. This is what will show up in the pg-boss DB tables.
|
||||
* @param {object} defaultJobOptions - Default options passed to `boss.send()`.
|
||||
* Ref: https://github.com/timgit/pg-boss/blob/master/docs/readme.md#sendname-data-options
|
||||
* @param {int | string | date} startAfter - Defers job execution. See `delay()` below for more.
|
||||
*/
|
||||
constructor(jobName, defaultJobOptions, startAfter = undefined) {
|
||||
super(jobName, PG_BOSS_EXECUTOR_NAME)
|
||||
this.#defaultJobOptions = defaultJobOptions
|
||||
this.#startAfter = startAfter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {int | string | date} startAfter - Defers job execution by either:
|
||||
* - int: Seconds to delay starting the job [Default: 0]
|
||||
* - string: Start after a UTC Date time string in 8601 format
|
||||
* - Date: Start after a Date object
|
||||
*/
|
||||
delay(startAfter) {
|
||||
return new PgBossJob(this.jobName, this.#defaultJobOptions, startAfter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the job to pg-boss.
|
||||
* @param {object} jobArgs - The job arguments supplied by the user for their perform callback.
|
||||
* @param {object} jobOptions - pg-boss specific options for `boss.send()`, which can override their defaultJobOptions.
|
||||
*/
|
||||
async submit(jobArgs, jobOptions = {}) {
|
||||
const boss = await pgBossStarted
|
||||
const jobId = await boss.send(this.jobName, jobArgs,
|
||||
{ ...this.#defaultJobOptions, ...(this.#startAfter && { startAfter: this.#startAfter }), ...jobOptions })
|
||||
return new PgBossSubmittedJob(boss, this, jobId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of PgBossJob and initializes the PgBoss executor by registering this job function.
|
||||
* We expect this to be called once per job name. If called multiple times with the same name and different
|
||||
* functions, we will override the previous calls.
|
||||
* @param {string} jobName - The user-defined job name in their .wasp file.
|
||||
* @param {fn} jobFn - The user-defined async job callback function.
|
||||
* @param {object} defaultJobOptions - pg-boss specific options for `boss.send()` applied to every `submit()` invocation,
|
||||
* which can overriden in that call.
|
||||
* @param {object} jobSchedule [Optional] - The 5-field cron string, job function JSON arg, and `boss.send()` options when invoking the job.
|
||||
* @param {array} entities - Entities used by job, passed into callback context.
|
||||
*/
|
||||
export function createJob({ jobName, jobFn, defaultJobOptions, jobSchedule, entities } = {}) {
|
||||
// NOTE(shayne): We are not awaiting `pgBossStarted` here since we need to return an instance to the job
|
||||
// template, or else the NodeJS module bootstrapping process will block and fail as it would then depend
|
||||
// on a runtime resolution of the promise in `startServer()`.
|
||||
// Since `pgBossStarted` will resolve in the future, it may appear possible to send pg-boss
|
||||
// a job before we actually have registered the handler via `boss.work()`. However, even if NodeJS does
|
||||
// not execute this callback before any job `submit()` calls, this is not a problem since pg-boss allows you
|
||||
// to submit jobs even if there are no workers registered.
|
||||
// Once they are registered, they will just start on the first job in their queue.
|
||||
pgBossStarted.then(async (boss) => {
|
||||
// As a safety precaution against undefined behavior of registering different
|
||||
// functions for the same job name, remove all registered functions first.
|
||||
await boss.offWork(jobName)
|
||||
|
||||
// This tells pg-boss to run given worker function when job with that name is submitted.
|
||||
// Ref: https://github.com/timgit/pg-boss/blob/master/docs/readme.md#work
|
||||
await boss.work(jobName, pgBossCallbackWrapper(jobFn, entities))
|
||||
|
||||
// If a job schedule is provided, we should schedule the recurring job.
|
||||
// If the schedule name already exists, it's updated to the provided cron expression, arguments, and options.
|
||||
// Ref: https://github.com/timgit/pg-boss/blob/master/docs/readme.md#scheduling
|
||||
if (jobSchedule) {
|
||||
const options = { ...defaultJobOptions, ...jobSchedule.options }
|
||||
await boss.schedule(jobName, jobSchedule.cron, jobSchedule.args || null, options)
|
||||
}
|
||||
})
|
||||
|
||||
return new PgBossJob(jobName, defaultJobOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the normal pg-boss callback function to inject entities, as well as extract
|
||||
* the `data` property so the arguments passed into the job are the exact same as those received.
|
||||
*
|
||||
* @param {fn} jobFn - The user-defined async job callback function.
|
||||
* @param {array} entities - Entities used by job, passed into callback context.
|
||||
* @returns a function that accepts the pg-boss callback arguments and invokes the user-defined callback.
|
||||
*/
|
||||
function pgBossCallbackWrapper(jobFn, entities) {
|
||||
return (args) => {
|
||||
const context = { entities }
|
||||
return jobFn(args.data, context)
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import { sleep } from '../../utils.js'
|
||||
import { Job } from './Job.js'
|
||||
import { SubmittedJob } from './SubmittedJob.js'
|
||||
|
||||
export const SIMPLE_EXECUTOR_NAME = Symbol('Simple')
|
||||
|
||||
/**
|
||||
* A simple job mainly intended for testing. It will not submit work to any
|
||||
* job executor, but instead will simply invoke the underlying perform function.
|
||||
* It does not support `schedule`. It does not require any extra NPM dependencies
|
||||
* or infrastructure, however.
|
||||
*/
|
||||
class SimpleJob extends Job {
|
||||
#jobFn
|
||||
#delaySeconds
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} jobName - Name of the Job.
|
||||
* @param {fn} jobFn - The Job function to execute.
|
||||
* @param {int} delaySeconds - The number of seconds to delay invoking the Job function.
|
||||
*/
|
||||
constructor(jobName, jobFn, delaySeconds = 0) {
|
||||
super(jobName, SIMPLE_EXECUTOR_NAME)
|
||||
this.#jobFn = jobFn
|
||||
this.#delaySeconds = delaySeconds
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {int} delaySeconds - Used to delay the processing of the job by some number of seconds.
|
||||
*/
|
||||
delay(delaySeconds) {
|
||||
return new SimpleJob(this.jobName, this.#jobFn, delaySeconds)
|
||||
}
|
||||
|
||||
async submit(jobArgs) {
|
||||
sleep(this.#delaySeconds * 1000).then(() => this.#jobFn(jobArgs))
|
||||
// NOTE: Dumb random ID generator, mainly so we don't have to add `uuid`
|
||||
// as a dependency in the server generator for something nobody will likely use.
|
||||
const jobId = (Math.random() + 1).toString(36).substring(7)
|
||||
return new SubmittedJob(this, jobId)
|
||||
}
|
||||
}
|
||||
|
||||
export function createJob({ jobName, jobFn } = {}) {
|
||||
return new SimpleJob(jobName, jobFn)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import prisma from '../dbClient.js'
|
||||
|
||||
import { foo } from '../ext-src/queries/bar.js'
|
||||
|
||||
|
||||
export default async function (args, context) {
|
||||
return foo(args, {
|
||||
...context,
|
||||
entities: {
|
||||
User: prisma.user,
|
||||
},
|
||||
})
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
|
||||
import {
|
||||
type _User,
|
||||
type AuthenticatedQuery,
|
||||
} from '../_types'
|
||||
|
||||
export type MySpecialQuery<Input = never, Output = unknown> =
|
||||
AuthenticatedQuery<
|
||||
[
|
||||
_User,
|
||||
],
|
||||
Input,
|
||||
Output
|
||||
>
|
||||
|
@ -0,0 +1,18 @@
|
||||
import express from 'express'
|
||||
|
||||
import auth from '../../core/auth.js'
|
||||
import login from './login.js'
|
||||
import signup from './signup.js'
|
||||
import me from './me.js'
|
||||
|
||||
import passportAuth from './passport/passport.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/login', login)
|
||||
router.post('/signup', signup)
|
||||
router.get('/me', auth, me)
|
||||
|
||||
router.use('/external', passportAuth)
|
||||
|
||||
export default router
|
@ -0,0 +1,40 @@
|
||||
import Prisma from '@prisma/client'
|
||||
import SecurePassword from 'secure-password'
|
||||
|
||||
import { sign, verifyPassword } from '../../core/auth.js'
|
||||
import { handleRejection } from '../../utils.js'
|
||||
|
||||
const prisma = new Prisma.PrismaClient()
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
const args = req.body || {}
|
||||
const context = {}
|
||||
|
||||
// Try to fetch user with the given username.
|
||||
const user = await prisma.user.findUnique({ where: { username: args.username } })
|
||||
if (!user) {
|
||||
return res.status(401).send()
|
||||
}
|
||||
|
||||
// We got user - now check the password.
|
||||
const verifyPassRes = await verifyPassword(user.password, args.password)
|
||||
switch (verifyPassRes) {
|
||||
case SecurePassword.VALID:
|
||||
break
|
||||
case SecurePassword.VALID_NEEDS_REHASH:
|
||||
// TODO(matija): take neccessary steps to make the password more secure.
|
||||
break
|
||||
default:
|
||||
return res.status(401).send()
|
||||
}
|
||||
|
||||
// Username & password valid - generate token.
|
||||
const token = await sign(user.id)
|
||||
|
||||
// NOTE(matija): Possible option - instead of explicitly returning token here,
|
||||
// we could add to response header 'Set-Cookie {token}' directive which would then make
|
||||
// browser automatically save cookie with token.
|
||||
// NOTE(shayne): Cross-domain cookies have serious limitations, which we recently explored.
|
||||
|
||||
return res.json({ token })
|
||||
})
|
@ -0,0 +1,9 @@
|
||||
import { handleRejection } from '../../utils.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
if (req.user) {
|
||||
return res.json(req.user)
|
||||
} else {
|
||||
return res.status(401).send()
|
||||
}
|
||||
})
|
@ -0,0 +1,62 @@
|
||||
import express from 'express'
|
||||
import passport from 'passport'
|
||||
|
||||
import waspServerConfig from '../../../../config.js'
|
||||
import { contextWithUserEntity, authConfig, findOrCreateUserByExternalAuthAssociation } from '../../utils.js'
|
||||
import { sign } from '../../../../core/auth.js'
|
||||
|
||||
// This function is invoked after we successfully exchange the one-time-use OAuth code for a real provider API token.
|
||||
// This token was used to get the provider profile information supplied as a parameter.
|
||||
// We add the provider profile to the request for downstream use.
|
||||
async function addProviderProfileToRequest(req, _accessToken, _refreshToken, providerProfile, done) {
|
||||
req.wasp = { ...req.wasp, providerProfile }
|
||||
done(null, {})
|
||||
}
|
||||
|
||||
export function initRouter(providerName, ProviderStrategy, config, getUserFieldsFn) {
|
||||
// Configure and use Passport.js strategy.
|
||||
const passportStrategyName = `wasp${providerName}LoginStrategy`
|
||||
const requiredConfig = {
|
||||
callbackURL: `${waspServerConfig.frontendUrl}/auth/login/${providerName}`,
|
||||
passReqToCallback: true
|
||||
}
|
||||
const passportStrategy = new ProviderStrategy({ ...config, ...requiredConfig }, addProviderProfileToRequest)
|
||||
|
||||
passport.use(passportStrategyName, passportStrategy)
|
||||
|
||||
// Create a new router to use the Passport.js strategy.
|
||||
const router = express.Router()
|
||||
|
||||
// Constructs a provider OAuth URL and redirects browser to start sign in flow.
|
||||
router.get('/login', passport.authenticate(passportStrategyName, { session: false }))
|
||||
|
||||
// Validates the OAuth code from the frontend, via server-to-server communication
|
||||
// with provider. If valid, provides frontend a response containing the JWT.
|
||||
// NOTE: `addProviderProfileToRequest` is invoked as part of the `passport.authenticate`
|
||||
// call, before the final route handler callback. This is how we gain access to `req.wasp.providerProfile`.
|
||||
router.get('/validateCodeForLogin',
|
||||
passport.authenticate(passportStrategyName, {
|
||||
session: false,
|
||||
failureRedirect: waspServerConfig.frontendUrl + authConfig.failureRedirectPath
|
||||
}),
|
||||
async function (req, res) {
|
||||
const providerProfile = req?.wasp?.providerProfile
|
||||
|
||||
if (!providerProfile) {
|
||||
throw new Error(`Missing ${providerName} provider profile on request. This should not happen! Please contact Wasp.`)
|
||||
} else if (!providerProfile.id) {
|
||||
throw new Error(`${providerName} provider profile was missing required id property. This should not happen! Please contact Wasp.`)
|
||||
}
|
||||
|
||||
// Wrap call to getUserFieldsFn so we can invoke only if needed.
|
||||
const getUserFields = () => getUserFieldsFn(contextWithUserEntity, { profile: providerProfile })
|
||||
// TODO: In the future we could make this configurable, possibly associating an external account
|
||||
// with the currently logged in account, or by some DB lookup.
|
||||
const user = await findOrCreateUserByExternalAuthAssociation(providerName, providerProfile.id, getUserFields)
|
||||
|
||||
const token = await sign(user.id)
|
||||
res.json({ token })
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { configFn } from './configMapping.js'
|
||||
export { getUserFieldsFn } from './configMapping.js'
|
||||
|
||||
// Validates the provided config function returns all required data.
|
||||
export const config = ((config) => {
|
||||
if (!config?.clientID) {
|
||||
throw new Error("The Google configFn must return an object with a clientID property.")
|
||||
}
|
||||
|
||||
if (!config?.clientSecret) {
|
||||
throw new Error("The Google configFn must return an object with a clientSecret property.")
|
||||
}
|
||||
|
||||
if (!config?.scope) {
|
||||
throw new Error("The Google configFn must return an object with a scope property.")
|
||||
} else if (!Array.isArray(config.scope) || !config.scope.includes('profile')) {
|
||||
throw new Error("The Google configFn returned an object with an invalid scope property. It must be an array including 'profile'.")
|
||||
}
|
||||
|
||||
return config
|
||||
})(await configFn())
|
@ -0,0 +1,4 @@
|
||||
|
||||
export { configFn } from './defaults.js'
|
||||
|
||||
export { getUserFieldsFn } from './defaults.js'
|
@ -0,0 +1,23 @@
|
||||
import { generateAvailableDictionaryUsername } from '../../../../core/auth.js'
|
||||
|
||||
// Default implementation if there is no `auth.methods.google.configFn`.
|
||||
export function configFn() {
|
||||
const clientID = process.env['GOOGLE_CLIENT_ID']
|
||||
const clientSecret = process.env['GOOGLE_CLIENT_SECRET']
|
||||
|
||||
if (!clientID) {
|
||||
throw new Error("Missing GOOGLE_CLIENT_ID environment variable.")
|
||||
}
|
||||
|
||||
if (!clientSecret) {
|
||||
throw new Error("Missing GOOGLE_CLIENT_SECRET environment variable.")
|
||||
}
|
||||
|
||||
return { clientID, clientSecret, scope: ['profile'] }
|
||||
}
|
||||
|
||||
// Default implementation if there is no `auth.methods.google.getUserFieldsFn`.
|
||||
export async function getUserFieldsFn(_context, _args) {
|
||||
const username = await generateAvailableDictionaryUsername()
|
||||
return { username }
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import express from 'express'
|
||||
import { initRouter } from './generic/provider.js'
|
||||
|
||||
const providerMap = new Map();
|
||||
providerMap.set('google', { npmPackage: 'passport-google-oauth20', passportImportPath: './google/config.js' })
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
async function initProviders(providers) {
|
||||
for (let [providerSlug, { npmPackage, passportImportPath }] of providers) {
|
||||
const { config, getUserFieldsFn } = await import(passportImportPath)
|
||||
const ProviderStrategy = await import(npmPackage)
|
||||
router.use(`/${providerSlug}`, initRouter(providerSlug, ProviderStrategy.default, config, getUserFieldsFn))
|
||||
}
|
||||
}
|
||||
|
||||
await initProviders(providerMap)
|
||||
|
||||
export default router
|
@ -0,0 +1,22 @@
|
||||
import prisma from '../../dbClient.js'
|
||||
import { handleRejection, isPrismaError, prismaErrorToHttpError } from '../../utils.js'
|
||||
import AuthError from '../../core/AuthError.js'
|
||||
import HttpError from '../../core/HttpError.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
const userFields = req.body || {}
|
||||
|
||||
try {
|
||||
await prisma.user.create({ data: userFields })
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) {
|
||||
throw new HttpError(422, 'Validation failed', { message: e.message })
|
||||
} else if (isPrismaError(e)) {
|
||||
throw prismaErrorToHttpError(e)
|
||||
} else {
|
||||
throw new HttpError(500)
|
||||
}
|
||||
}
|
||||
|
||||
res.send()
|
||||
})
|
@ -0,0 +1,41 @@
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import prisma from '../../dbClient.js'
|
||||
|
||||
export const contextWithUserEntity = {
|
||||
entities: {
|
||||
User: prisma.user
|
||||
}
|
||||
}
|
||||
|
||||
export const authConfig = {
|
||||
failureRedirectPath: "/login",
|
||||
successRedirectPath: "/",
|
||||
}
|
||||
|
||||
export async function findOrCreateUserByExternalAuthAssociation(provider, providerId, getUserFields) {
|
||||
// Attempt to find a User by an external auth association.
|
||||
const externalAuthAssociation = await prisma.socialLogin.findFirst({
|
||||
where: { provider, providerId },
|
||||
include: { user: true }
|
||||
})
|
||||
|
||||
if (externalAuthAssociation) {
|
||||
return externalAuthAssociation.user
|
||||
}
|
||||
|
||||
// No external auth association linkage found. Create a new User using details from
|
||||
// `getUserFields()`. Additionally, associate the externalAuthAssociations with the new User.
|
||||
// NOTE: For now, we force a random (uuidv4) password string. In the future, we will allow password reset.
|
||||
const userFields = await getUserFields()
|
||||
const userAndExternalAuthAssociation = {
|
||||
...userFields,
|
||||
password: uuidv4(),
|
||||
externalAuthAssociations: {
|
||||
create: [{ provider, providerId }]
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.user.create({ data: userAndExternalAuthAssociation })
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import express from 'express'
|
||||
import operations from './operations/index.js'
|
||||
import auth from './auth/index.js'
|
||||
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
res.json('Hello world')
|
||||
})
|
||||
|
||||
router.use('/auth', auth)
|
||||
router.use('/operations', operations)
|
||||
|
||||
export default router
|
@ -0,0 +1,14 @@
|
||||
import { handleRejection } from '../../utils.js'
|
||||
|
||||
import MySpecialAction from '../../actions/MySpecialAction.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
const args = req.body || {}
|
||||
|
||||
const context = {
|
||||
user: req.user
|
||||
}
|
||||
const result = await MySpecialAction(args, context)
|
||||
res.json(result)
|
||||
})
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { handleRejection } from '../../utils.js'
|
||||
|
||||
import MySpecialQuery from '../../queries/MySpecialQuery.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
const args = req.body || {}
|
||||
|
||||
const context = {
|
||||
user: req.user
|
||||
}
|
||||
|
||||
const result = await MySpecialQuery(args, context)
|
||||
res.json(result)
|
||||
})
|
@ -0,0 +1,13 @@
|
||||
import express from 'express'
|
||||
|
||||
import auth from '../../core/auth.js'
|
||||
|
||||
import MySpecialAction from './MySpecialAction.js'
|
||||
import MySpecialQuery from './MySpecialQuery.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/my-special-action', auth, MySpecialAction)
|
||||
router.post('/my-special-query', auth, MySpecialQuery)
|
||||
|
||||
export default router
|
58
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/server.ts
generated
Normal file
58
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/server.ts
generated
Normal file
@ -0,0 +1,58 @@
|
||||
import http from 'http'
|
||||
|
||||
import app from './app.js'
|
||||
import config from './config.js'
|
||||
|
||||
import mySetupFunction from './ext-src/myServerSetupCode.js'
|
||||
import { ServerSetupFnContext } from './types'
|
||||
|
||||
import { startPgBoss } from './jobs/core/pgBoss/pgBoss.js'
|
||||
import './jobs/core/allJobs.js'
|
||||
|
||||
const startServer = async () => {
|
||||
await startPgBoss()
|
||||
|
||||
const port = normalizePort(config.port)
|
||||
app.set('port', port)
|
||||
|
||||
const server = http.createServer(app)
|
||||
|
||||
const serverSetupFnContext: ServerSetupFnContext = { app, server }
|
||||
await mySetupFunction(serverSetupFnContext)
|
||||
|
||||
server.listen(port)
|
||||
|
||||
server.on('error', (error: NodeJS.ErrnoException) => {
|
||||
if (error.syscall !== 'listen') throw error
|
||||
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges')
|
||||
process.exit(1)
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use')
|
||||
process.exit(1)
|
||||
default:
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
server.on('listening', () => {
|
||||
const addr = server.address()
|
||||
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
|
||||
console.log('Server listening on ' + bind)
|
||||
})
|
||||
}
|
||||
|
||||
startServer().catch(e => console.error(e))
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
function normalizePort (val) {
|
||||
const port = parseInt(val, 10)
|
||||
if (isNaN(port)) return val // named pipe
|
||||
if (port >= 0) return port // port number
|
||||
return false
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { Application } from 'express'
|
||||
import { Server } from 'http'
|
||||
|
||||
export type ServerSetupFn = (context: ServerSetupFnContext) => Promise<void>
|
||||
|
||||
export type ServerSetupFnContext = {
|
||||
app: Application,
|
||||
server: Server,
|
||||
}
|
||||
|
||||
export { Application } from 'express'
|
||||
export { Server } from 'http'
|
@ -0,0 +1,3 @@
|
||||
export function stripTrailingSlash(url?: string): string | undefined {
|
||||
return url?.replace(/\/$/, "");
|
||||
}
|
47
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.js
generated
Normal file
47
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.js
generated
Normal file
@ -0,0 +1,47 @@
|
||||
import Prisma from '@prisma/client'
|
||||
import HttpError from './core/HttpError.js'
|
||||
|
||||
/**
|
||||
* Decorator for async express middleware that handles promise rejections.
|
||||
* @param {Func} middleware - Express middleware function.
|
||||
* @returns {Func} Express middleware that is exactly the same as the given middleware but,
|
||||
* if given middleware returns promise, reject of that promise will be correctly handled,
|
||||
* meaning that error will be forwarded to next().
|
||||
*/
|
||||
export const handleRejection = (middleware) => async (req, res, next) => {
|
||||
try {
|
||||
await middleware(req, res, next)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
export const isPrismaError = (e) => {
|
||||
return e instanceof Prisma.PrismaClientKnownRequestError ||
|
||||
e instanceof Prisma.PrismaClientUnknownRequestError ||
|
||||
e instanceof Prisma.PrismaClientRustPanicError ||
|
||||
e instanceof Prisma.PrismaClientInitializationError ||
|
||||
e instanceof Prisma.PrismaClientValidationError
|
||||
}
|
||||
|
||||
export const prismaErrorToHttpError = (e) => {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (e.code === 'P2002') {
|
||||
return new HttpError(422, 'Save failed', {
|
||||
message: `A record with the same ${e.meta.target.join(', ')} already exists.`,
|
||||
target: e.meta.target
|
||||
})
|
||||
} else {
|
||||
// TODO(shayne): Go through https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes
|
||||
// and decide which are input errors (422) and which are not (500)
|
||||
// See: https://github.com/wasp-lang/wasp/issues/384
|
||||
return new HttpError(500)
|
||||
}
|
||||
} else if (e instanceof Prisma.PrismaClientValidationError) {
|
||||
return new HttpError(422, 'Save failed')
|
||||
} else {
|
||||
return new HttpError(500)
|
||||
}
|
||||
}
|
||||
|
||||
export const sleep = ms => new Promise(r => setTimeout(r, ms))
|
20
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/tsconfig.json
generated
Normal file
20
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/tsconfig.json
generated
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// Overriding this until we implement more complete TypeScript support.
|
||||
"strict": false,
|
||||
// Overriding this because we want to use top-level await
|
||||
"module": "esnext",
|
||||
// Enable source map for debugging
|
||||
"sourceMap": true,
|
||||
// The remaining settings should match node18/tsconfig.json, but I kept
|
||||
// them here to be explicit.
|
||||
|
||||
// Enable default imports in TypeScript.
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist",
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
1
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/.npmrc
generated
Normal file
1
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/.npmrc
generated
Normal file
@ -0,0 +1 @@
|
||||
engine-strict=true
|
68
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/README.md
generated
Normal file
68
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/README.md
generated
Normal file
@ -0,0 +1,68 @@
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br>
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br>
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
|
24
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/index.html
generated
Normal file
24
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/index.html
generated
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
|
||||
|
||||
<title>waspComplexTest</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,8 @@
|
||||
[build]
|
||||
publish = "build/"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
force = false
|
47
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/package.json
generated
Normal file
47
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/package.json
generated
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"browserslist": {
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
],
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "4.5.0",
|
||||
"@tanstack/react-query": "^4.13.0",
|
||||
"axios": "^0.27.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"redux": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/vite-react": "^1.0.1",
|
||||
"@types/react": "^17.0.53",
|
||||
"@types/react-dom": "^17.0.19",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.1.0"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"engines": {
|
||||
"node": "^18.12.0"
|
||||
},
|
||||
"name": "waspComplexTest",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npm run validate-env && tsc && vite build",
|
||||
"start": "npm run validate-env && vite",
|
||||
"validate-env": "node -r dotenv/config ./scripts/validate-env.mjs"
|
||||
},
|
||||
"type": "module",
|
||||
"version": "0.0.0"
|
||||
}
|
BIN
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/public/favicon.ico
generated
Normal file
BIN
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/public/favicon.ico
generated
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "waspComplexTest",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
export function isValidAbsoluteURL(rawUrl) {
|
||||
try {
|
||||
const url = new URL(rawUrl);
|
||||
/*
|
||||
URL constructor will parse some invalid absolute URLs as valid URLs
|
||||
so we need to do some extra checks.
|
||||
|
||||
Example: "localhost:3000" will be parsed as URL with protocol of
|
||||
"localhost:" and host of "3000"
|
||||
*/
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function throwIfNotValidAbsoluteURL(value, name) {
|
||||
if (value && !isValidAbsoluteURL(value)) {
|
||||
throw new Error(`${name} must be a valid absolute URL`);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import { throwIfNotValidAbsoluteURL } from './universal/validators.mjs';
|
||||
|
||||
console.info("🔍 Validating environment variables...");
|
||||
throwIfNotValidAbsoluteURL(process.env.REACT_APP_API_URL, 'Environemnt variable REACT_APP_API_URL');
|
@ -0,0 +1,6 @@
|
||||
import { createAction } from './core'
|
||||
|
||||
export default createAction(
|
||||
'operations/my-special-action',
|
||||
['User'],
|
||||
)
|
3
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/actions/core.d.ts
generated
vendored
Normal file
3
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/actions/core.d.ts
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Action } from '.'
|
||||
|
||||
export function createAction<Input, Output>(actionRoute: string, entitiesUsed: unknown[]): Action<Input, Output>
|
@ -0,0 +1,33 @@
|
||||
import { callOperation } from '../operations'
|
||||
import {
|
||||
registerActionInProgress,
|
||||
registerActionDone,
|
||||
} from '../operations/resources'
|
||||
|
||||
export function createAction(actionRoute, entitiesUsed) {
|
||||
async function internalAction(args, specificOptimisticUpdateDefinitions) {
|
||||
registerActionInProgress(specificOptimisticUpdateDefinitions)
|
||||
try {
|
||||
// The `return await` is not redundant here. If we removed the await, the
|
||||
// `finally` block would execute before the action finishes, prematurely
|
||||
// registering the action as done.
|
||||
return await callOperation(actionRoute, args)
|
||||
} finally {
|
||||
await registerActionDone(entitiesUsed, specificOptimisticUpdateDefinitions)
|
||||
}
|
||||
}
|
||||
|
||||
// We expose (and document) a restricted version of the API for our users,
|
||||
// while also attaching the full "internal" API to the exposed action. By
|
||||
// doing this, we can easily use the internal API of an action a users passes
|
||||
// into our system (e.g., through the `useAction` hook) without needing a
|
||||
// lookup table.
|
||||
//
|
||||
// While it does technically allow our users to access the interal API, it
|
||||
// shouldn't be a problem in practice. Still, if it turns out to be a problem,
|
||||
// we can always hide it using a Symbol.
|
||||
const action = (args) => internalAction(args, [])
|
||||
action.internal = internalAction
|
||||
|
||||
return action
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
import {
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { Query } from '../queries';
|
||||
|
||||
export type Action<Input, Output> = (args?: Input) => Promise<Output>;
|
||||
|
||||
/**
|
||||
* An options object passed into the `useAction` hook and used to enhance the
|
||||
* action with extra options.
|
||||
*
|
||||
*/
|
||||
export type ActionOptions<ActionInput> = {
|
||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, any>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A documented (public) way to define optimistic updates.
|
||||
*/
|
||||
export type OptimisticUpdateDefinition<ActionInput, CachedData> = {
|
||||
getQuerySpecifier: GetQuerySpecifier<ActionInput, CachedData>
|
||||
updateQuery: UpdateQuery<ActionInput, CachedData>
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that takes an item and returns a Wasp Query specifier.
|
||||
*/
|
||||
export type GetQuerySpecifier<ActionInput, CachedData> = (item: ActionInput) => QuerySpecifier<unknown, CachedData>
|
||||
|
||||
/**
|
||||
* A function that takes an item and the previous state of the cache, and returns
|
||||
* the desired (new) state of the cache.
|
||||
*/
|
||||
export type UpdateQuery<ActionInput, CachedData> = (item: ActionInput, oldData: CachedData | undefined) => CachedData
|
||||
|
||||
/**
|
||||
* A public query specifier used for addressing Wasp queries. See our docs for details:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
*/
|
||||
export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
|
||||
/**
|
||||
* A hook for adding extra behavior to a Wasp Action (e.g., optimistic updates).
|
||||
*
|
||||
* @param actionFn The Wasp Action you wish to enhance/decorate.
|
||||
* @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>(
|
||||
actionFn: Action<Input, Output>,
|
||||
actionOptions?: ActionOptions<Input>
|
||||
): typeof actionFn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
let mutationFn = actionFn
|
||||
let options = {}
|
||||
if (actionOptions?.optimisticUpdates) {
|
||||
const optimisticUpdatesDefinitions = actionOptions.optimisticUpdates.map(translateToInternalDefinition)
|
||||
mutationFn = makeOptimisticUpdateMutationFn(actionFn, optimisticUpdatesDefinitions)
|
||||
options = makeRqOptimisticUpdateOptions(queryClient, optimisticUpdatesDefinitions)
|
||||
}
|
||||
|
||||
// NOTE: We decided to hide React Query's extra mutation features (e.g.,
|
||||
// isLoading, onSuccess and onError callbacks, synchronous mutate) and only
|
||||
// expose a simple async function whose API matches the original Action.
|
||||
// We did this to avoid cluttering the API with stuff we're not sure we need
|
||||
// yet (e.g., isLoading), to postpone the action vs mutation dilemma, and to
|
||||
// clearly separate our opinionated API from React Query's lower-level
|
||||
// advanced API (which users can also use)
|
||||
const mutation = useMutation(mutationFn, options)
|
||||
return (args) => mutation.mutateAsync(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* An internal (undocumented, private, desugared) way of defining optimistic updates.
|
||||
*/
|
||||
type InternalOptimisticUpdateDefinition<ActionInput, CachedData> = {
|
||||
getQueryKey: (item: ActionInput) => QueryKey,
|
||||
updateQuery: UpdateQuery<ActionInput, CachedData>;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An UpdateQuery function "instantiated" with a specific item. It only takes
|
||||
* the current state of the cache and returns the desired (new) state of the
|
||||
* cache.
|
||||
*/
|
||||
type SpecificUpdateQuery<CachedData> = (oldData: CachedData) => CachedData
|
||||
|
||||
/**
|
||||
* A specific, "instantiated" optimistic update definition which contains a
|
||||
* fully-constructed query key and a specific update function.
|
||||
*/
|
||||
type SpecificOptimisticUpdateDefinition<Item> = {
|
||||
queryKey: QueryKey;
|
||||
updateQuery: SpecificUpdateQuery<Item>;
|
||||
}
|
||||
|
||||
type InternalAction<Input, Output> = Action<Input, Output> & {
|
||||
internal<CachedData extends unknown>(
|
||||
item: Input,
|
||||
optimisticUpdateDefinitions: SpecificOptimisticUpdateDefinition<CachedData>[]
|
||||
): Promise<Output>
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates/Desugars a public optimistic update definition object into a
|
||||
* definition object our system uses internally.
|
||||
*
|
||||
* @param publicOptimisticUpdateDefinition An optimistic update definition
|
||||
* object that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* @returns An internally-used optimistic update definition object.
|
||||
*/
|
||||
function translateToInternalDefinition<Item, CachedData>(
|
||||
publicOptimisticUpdateDefinition: OptimisticUpdateDefinition<Item, CachedData>
|
||||
): InternalOptimisticUpdateDefinition<Item, CachedData> {
|
||||
const { getQuerySpecifier, updateQuery } = publicOptimisticUpdateDefinition
|
||||
|
||||
const definitionErrors = []
|
||||
if (typeof getQuerySpecifier !== 'function') {
|
||||
definitionErrors.push('`getQuerySpecifier` is not a function.')
|
||||
}
|
||||
if (typeof updateQuery !== 'function') {
|
||||
definitionErrors.push('`updateQuery` is not a function.')
|
||||
}
|
||||
if (definitionErrors.length) {
|
||||
throw new TypeError(`Invalid optimistic update definition: ${definitionErrors.join(', ')}.`)
|
||||
}
|
||||
|
||||
return {
|
||||
getQueryKey: (item) => getRqQueryKeyFromSpecifier(getQuerySpecifier(item)),
|
||||
updateQuery,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function that performs an action while telling it about the
|
||||
* optimistic updates it caused.
|
||||
*
|
||||
* @param actionFn The Wasp Action.
|
||||
* @param optimisticUpdateDefinitions The optimisitc updates the action causes.
|
||||
* @returns An decorated action which performs optimistic updates.
|
||||
*/
|
||||
function makeOptimisticUpdateMutationFn<Input, Output, CachedData>(
|
||||
actionFn: Action<Input, Output>,
|
||||
optimisticUpdateDefinitions: InternalOptimisticUpdateDefinition<Input, CachedData>[]
|
||||
): typeof actionFn {
|
||||
return function performActionWithOptimisticUpdates(item) {
|
||||
const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map(
|
||||
generalDefinition => getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item)
|
||||
)
|
||||
return (actionFn as InternalAction<Input, Output>).internal(item, specificOptimisticUpdateDefinitions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a ReactQuery query client and our internal definition of optimistic
|
||||
* updates, this function constructs an object describing those same optimistic
|
||||
* updates in a format we can pass into React Query's useMutation hook. In other
|
||||
* words, it translates our optimistic updates definition into React Query's
|
||||
* optimistic updates definition. Check their docs for details:
|
||||
* https://tanstack.com/query/v4/docs/guides/optimistic-updates?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/optimistic-updates
|
||||
*
|
||||
* @param queryClient The QueryClient instance used by React Query.
|
||||
* @param optimisticUpdateDefinitions A list containing internal optimistic
|
||||
* updates definition objects (i.e., a list where each object carries the
|
||||
* instructions for performing particular optimistic update).
|
||||
* @returns An object containing 'onMutate' and 'onError' functions
|
||||
* corresponding to the given optimistic update definitions (check the docs
|
||||
* linked above for details).
|
||||
*/
|
||||
function makeRqOptimisticUpdateOptions<ActionInput, CachedData>(
|
||||
queryClient: QueryClient,
|
||||
optimisticUpdateDefinitions: InternalOptimisticUpdateDefinition<ActionInput, CachedData>[]
|
||||
): Pick<UseMutationOptions, "onMutate" | "onError"> {
|
||||
async function onMutate(item) {
|
||||
const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map(
|
||||
generalDefinition => getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item)
|
||||
)
|
||||
|
||||
// Cancel any outgoing refetches (so they don't overwrite our optimistic update).
|
||||
// Theoretically, we can be a bit faster. Instead of awaiting the
|
||||
// cancellation of all queries, we could cancel and update them in parallel.
|
||||
// However, awaiting cancellation hasn't yet proven to be a performance bottleneck.
|
||||
await Promise.all(specificOptimisticUpdateDefinitions.map(
|
||||
({ queryKey }) => queryClient.cancelQueries(queryKey)
|
||||
))
|
||||
|
||||
// We're using a Map to correctly serialize query keys that contain objects.
|
||||
const previousData = new Map()
|
||||
specificOptimisticUpdateDefinitions.forEach(({ queryKey, updateQuery }) => {
|
||||
// Snapshot the currently cached value.
|
||||
const previousDataForQuery: CachedData = queryClient.getQueryData(queryKey)
|
||||
|
||||
// Attempt to optimistically update the cache using the new value.
|
||||
try {
|
||||
queryClient.setQueryData(queryKey, updateQuery)
|
||||
} catch (e) {
|
||||
console.error("The `updateQuery` function threw an exception, skipping optimistic update:")
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
// Remember the snapshotted value to restore in case of an error.
|
||||
previousData.set(queryKey, previousDataForQuery)
|
||||
})
|
||||
|
||||
return { previousData }
|
||||
}
|
||||
|
||||
function onError(_err, _item, context) {
|
||||
// All we do in case of an error is roll back all optimistic updates. We ensure
|
||||
// not to do anything else because React Query rethrows the error. This allows
|
||||
// the programmer to handle the error as they usually would (i.e., we want the
|
||||
// error handling to work as it would if the programmer wasn't using optimistic
|
||||
// updates).
|
||||
context.previousData.forEach(async (data, queryKey) => {
|
||||
await queryClient.cancelQueries(queryKey)
|
||||
queryClient.setQueryData(queryKey, data)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
onMutate,
|
||||
onError,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the definition for optimistically updating a specific item. It
|
||||
* uses a closure over the updated item to construct an item-specific query key
|
||||
* (e.g., useful when the query key depends on an ID).
|
||||
*
|
||||
* @param optimisticUpdateDefinition The general, "uninstantiated" optimistic
|
||||
* update definition with a function for constructing the query key.
|
||||
* @param item The item triggering the Action/optimistic update (i.e., the
|
||||
* argument passed to the Action).
|
||||
* @returns A specific optimistic update definition which corresponds to the
|
||||
* provided definition and closes over the provided item.
|
||||
*/
|
||||
function getOptimisticUpdateDefinitionForSpecificItem<ActionInput, CachedData>(
|
||||
optimisticUpdateDefinition: InternalOptimisticUpdateDefinition<ActionInput, CachedData>,
|
||||
item: ActionInput
|
||||
): SpecificOptimisticUpdateDefinition<CachedData> {
|
||||
const { getQueryKey, updateQuery } = optimisticUpdateDefinition
|
||||
return {
|
||||
queryKey: getQueryKey(item),
|
||||
updateQuery: (old) => updateQuery(item, old)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a Wasp query specifier to a query cache key used by React Query.
|
||||
*
|
||||
* @param querySpecifier A query specifier that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* @returns A cache key React Query internally uses for addressing queries.
|
||||
*/
|
||||
function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier<unknown, unknown>): QueryKey {
|
||||
const [queryFn, ...otherKeys] = querySpecifier
|
||||
return [...(queryFn as any).queryCacheKey, ...otherKeys]
|
||||
}
|
74
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api.js
generated
Normal file
74
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api.js
generated
Normal file
@ -0,0 +1,74 @@
|
||||
import axios from 'axios'
|
||||
import config from './config'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: config.apiUrl,
|
||||
})
|
||||
|
||||
const WASP_APP_AUTH_TOKEN_NAME = "authToken"
|
||||
|
||||
let authToken = null
|
||||
if (window.localStorage) {
|
||||
authToken = window.localStorage.getItem(WASP_APP_AUTH_TOKEN_NAME)
|
||||
}
|
||||
|
||||
export const setAuthToken = (token) => {
|
||||
if (typeof token !== 'string') {
|
||||
throw Error(`Token must be a string, but it was: {${typeof token}} ${token}.`)
|
||||
}
|
||||
authToken = token
|
||||
window.localStorage && window.localStorage.setItem(WASP_APP_AUTH_TOKEN_NAME, token)
|
||||
}
|
||||
|
||||
export const clearAuthToken = () => {
|
||||
authToken = undefined
|
||||
window.localStorage && window.localStorage.removeItem(WASP_APP_AUTH_TOKEN_NAME)
|
||||
}
|
||||
|
||||
export const clearLocalStorage = () => {
|
||||
authToken = undefined
|
||||
|
||||
window.localStorage && window.localStorage.clear()
|
||||
}
|
||||
|
||||
api.interceptors.request.use(request => {
|
||||
if (authToken) {
|
||||
request.headers['Authorization'] = `Bearer ${authToken}`
|
||||
}
|
||||
return request
|
||||
})
|
||||
|
||||
api.interceptors.response.use(undefined, error => {
|
||||
if (error.response?.status === 401) {
|
||||
clearAuthToken()
|
||||
}
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
/**
|
||||
* Takes an error returned by the app's API (as returned by axios), and transforms into a more
|
||||
* standard format to be further used by the client. It is also assumed that given API
|
||||
* error has been formatted as implemented by HttpError on the server.
|
||||
*/
|
||||
export const handleApiError = (error) => {
|
||||
if (error?.response) {
|
||||
// If error came from HTTP response, we capture most informative message
|
||||
// and also add .statusCode information to it.
|
||||
// If error had JSON response, we assume it is of format { message, data } and
|
||||
// add that info to the error.
|
||||
// TODO: We might want to use HttpError here instead of just Error, since
|
||||
// HttpError is also used on server to throw errors like these.
|
||||
// That would require copying HttpError code to web-app also and using it here.
|
||||
const responseJson = error.response?.data
|
||||
const responseStatusCode = error.response.status
|
||||
const e = new Error(responseJson?.message || error.message)
|
||||
e.statusCode = responseStatusCode
|
||||
e.data = responseJson?.data
|
||||
throw e
|
||||
} else {
|
||||
// If any other error, we just propagate it.
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
@ -0,0 +1,46 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import login from '../login.js'
|
||||
import { errorMessage } from '../../utils.js'
|
||||
|
||||
const LoginForm = () => {
|
||||
const history = useHistory()
|
||||
|
||||
const [usernameFieldVal, setUsernameFieldVal] = useState('')
|
||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
||||
|
||||
const handleLogin = async (event) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await login(usernameFieldVal, passwordFieldVal)
|
||||
// Redirect to configured page, defaults to /.
|
||||
history.push('/')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
window.alert(errorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleLogin} className="login-form auth-form">
|
||||
<h2>Username</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={usernameFieldVal}
|
||||
onChange={e => setUsernameFieldVal(e.target.value)}
|
||||
/>
|
||||
<h2>Password</h2>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordFieldVal}
|
||||
onChange={e => setPasswordFieldVal(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<input type="submit" value="Log in"/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginForm
|
@ -0,0 +1,52 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import signup from '../signup.js'
|
||||
import login from '../login.js'
|
||||
import { errorMessage } from '../../utils.js'
|
||||
|
||||
const SignupForm = () => {
|
||||
const history = useHistory()
|
||||
|
||||
const [usernameFieldVal, setUsernameFieldVal] = useState('')
|
||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
||||
|
||||
const handleSignup = async (event) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await signup({ username: usernameFieldVal, password: passwordFieldVal })
|
||||
await login (usernameFieldVal, passwordFieldVal)
|
||||
|
||||
setUsernameFieldVal('')
|
||||
setPasswordFieldVal('')
|
||||
|
||||
// Redirect to configured page, defaults to /.
|
||||
history.push('/')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
window.alert(errorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSignup} className='signup-form auth-form'>
|
||||
<h2>Username</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={usernameFieldVal}
|
||||
onChange={e => setUsernameFieldVal(e.target.value)}
|
||||
/>
|
||||
<h2>Password</h2>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordFieldVal}
|
||||
onChange={e => setPasswordFieldVal(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<input type="submit" value="Sign up"/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignupForm
|
@ -0,0 +1,44 @@
|
||||
|
||||
import config from '../../config.js'
|
||||
|
||||
export const signInUrl = `${config.apiUrl}/auth/external/google/login`
|
||||
export const logoUrl = '/images/google-logo-icon.png'
|
||||
|
||||
const containerStyle = {
|
||||
border: '2px solid #cbd5e1',
|
||||
margin: 0,
|
||||
cursor: 'pointer',
|
||||
borderRadius: '.375rem',
|
||||
backgroundColor: '#f8fafc',
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
|
||||
outline: '2px solid transparent',
|
||||
outlineOffset: '2px',
|
||||
}
|
||||
|
||||
const linkStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textDecoration: 'none',
|
||||
color: '#1e293b',
|
||||
paddingLeft: '1.5rem',
|
||||
paddingRight: '1.5rem',
|
||||
paddingTop: '.75rem',
|
||||
paddingBottom: '.75rem',
|
||||
}
|
||||
|
||||
const logoStyle = {
|
||||
maxHeight: '24px',
|
||||
marginRight: '0.75rem'
|
||||
}
|
||||
|
||||
export function SignInButton() {
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<a href={signInUrl} style={linkStyle}>
|
||||
<img alt="Google Icon" src={logoUrl} style={logoStyle} />
|
||||
<span>Log in with Google</span>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { setAuthToken } from '../../api.js'
|
||||
import { invalidateAndRemoveQueries } from '../../operations/resources'
|
||||
|
||||
export async function initSession(token: string): Promise<void> {
|
||||
setAuthToken(token)
|
||||
// We need to invalidate queries after login in order to get the correct user
|
||||
// data in the React components (using `useAuth`).
|
||||
// Redirects after login won't work properly without this.
|
||||
|
||||
// TODO(filip): We are currently removing all the queries, but we should
|
||||
// remove only non-public, user-dependent queries - public queries are
|
||||
// expected not to change in respect to the currently logged in user.
|
||||
await invalidateAndRemoveQueries()
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import api, { handleApiError } from '../api.js'
|
||||
import { initSession } from './helpers/user'
|
||||
|
||||
export default async function login(username, password) {
|
||||
try {
|
||||
const args = { username, password }
|
||||
const response = await api.post('/auth/login', args)
|
||||
|
||||
await initSession(response.data.token)
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { clearLocalStorage } from '../api.js'
|
||||
import { invalidateAndRemoveQueries } from '../operations/resources'
|
||||
|
||||
export default async function logout() {
|
||||
clearLocalStorage()
|
||||
// TODO(filip): We are currently invalidating and removing all the queries, but
|
||||
// we should remove only the non-public, user-dependent ones.
|
||||
await invalidateAndRemoveQueries()
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import config from '../../config.js'
|
||||
import api from '../../api.js'
|
||||
import { initSession } from '../helpers/user'
|
||||
|
||||
// After a user authenticates via an Oauth 2.0 provider, this is the page that
|
||||
// the provider should redirect them to, while providing query string parameters
|
||||
// that contain information needed for the API server to authenticate with the provider.
|
||||
// This page forwards that information to the API server and in response get a JWT,
|
||||
// which it stores on the client, therefore completing the OAuth authentication process.
|
||||
export default function OAuthCodeExchange({ pathToApiServerRouteHandlingOauthRedirect }) {
|
||||
const history = useHistory()
|
||||
|
||||
useEffect(() => {
|
||||
// NOTE: Different auth methods will have different Wasp API server validation paths.
|
||||
// This helps us reuse one component for various methods (e.g., Google, Facebook, etc.).
|
||||
const apiServerUrlHandlingOauthRedirect = constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRedirect)
|
||||
|
||||
exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect)
|
||||
}, [history, pathToApiServerRouteHandlingOauthRedirect])
|
||||
|
||||
return (
|
||||
<p>Completing login process...</p>
|
||||
)
|
||||
}
|
||||
|
||||
function constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRedirect) {
|
||||
// Take the redirect query params supplied by the external OAuth provider and
|
||||
// send them as-is to our backend, so Passport can finish the process.
|
||||
const queryParams = window.location.search
|
||||
return `${config.apiUrl}${pathToApiServerRouteHandlingOauthRedirect}${queryParams}`
|
||||
}
|
||||
|
||||
async function exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect) {
|
||||
const token = await exchangeCodeForJwt(apiServerUrlHandlingOauthRedirect)
|
||||
|
||||
if (token !== null) {
|
||||
await initSession(token)
|
||||
history.push('/')
|
||||
} else {
|
||||
console.error('Error obtaining JWT token')
|
||||
history.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
async function exchangeCodeForJwt(url) {
|
||||
try {
|
||||
const response = await api.get(url)
|
||||
return response?.data?.token || null
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return null
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import useAuth from '../useAuth.js'
|
||||
|
||||
|
||||
const createAuthRequiredPage = (Page) => {
|
||||
return (props) => {
|
||||
const { data: user, isError, isSuccess, isLoading } = useAuth()
|
||||
|
||||
if (isSuccess) {
|
||||
if (user) {
|
||||
return (
|
||||
<Page {...props} user={user} />
|
||||
)
|
||||
} else {
|
||||
return <Redirect to="/login" />
|
||||
}
|
||||
} else if (isLoading) {
|
||||
return <span>Loading...</span>
|
||||
} else if (isError) {
|
||||
return <span>An error ocurred. Please refresh the page.</span>
|
||||
} else {
|
||||
return <span>An unknown error ocurred. Please refresh the page.</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default createAuthRequiredPage
|
||||
|
@ -0,0 +1,9 @@
|
||||
import api, { handleApiError } from '../api.js'
|
||||
|
||||
export default async function signup(userFields) {
|
||||
try {
|
||||
await api.post('/auth/signup', userFields)
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { useQuery } from '../queries'
|
||||
import api, { handleApiError } from '../api.js'
|
||||
|
||||
export default function useAuth(queryFnArgs, config) {
|
||||
return useQuery(getMe, queryFnArgs, config)
|
||||
}
|
||||
async function getMe() {
|
||||
try {
|
||||
const response = await api.get('/auth/me')
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
return null
|
||||
} else {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMe.queryCacheKey = ['auth/me']
|
@ -0,0 +1,9 @@
|
||||
import { stripTrailingSlash } from "./universal/url";
|
||||
|
||||
const apiUrl = stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || 'http://localhost:3001';
|
||||
|
||||
const config = {
|
||||
apiUrl,
|
||||
}
|
||||
|
||||
export default config
|
@ -0,0 +1,14 @@
|
||||
import {
|
||||
User,
|
||||
SocialLogin,
|
||||
} from '@prisma/client'
|
||||
|
||||
export type {
|
||||
User,
|
||||
SocialLogin,
|
||||
} from '@prisma/client'
|
||||
|
||||
export type Entity =
|
||||
| User
|
||||
| SocialLogin
|
||||
| never
|
@ -0,0 +1,9 @@
|
||||
export default function App({ children }) {
|
||||
return (
|
||||
<div className="app">
|
||||
<h1>Root component</h1>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,89 @@
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 5rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main p {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-weight: 400;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.buttons .button:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-radius: 3px;
|
||||
font-size: 1.2rem;
|
||||
padding: 1rem 2rem;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button-filled {
|
||||
border: 2px solid #bf9900;
|
||||
background-color: #bf9900;
|
||||
color: #f4f4f4;
|
||||
}
|
||||
|
||||
.button-outline {
|
||||
border: 2px solid #8a9cff;
|
||||
color: #8a9cff;
|
||||
background-color: none;
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 5px;
|
||||
padding: 0.2rem;
|
||||
background: #efefef;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import waspLogo from './waspLogo.png'
|
||||
import './Main.css'
|
||||
|
||||
const MainPage = () => {
|
||||
return (
|
||||
<div className="container">
|
||||
<main>
|
||||
<div className="logo">
|
||||
<img src={waspLogo} alt="wasp" />
|
||||
</div>
|
||||
|
||||
<h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2>
|
||||
<h3 className="welcome-subtitle">
|
||||
This is page <code>MainPage</code> located at route <code>/</code>.
|
||||
Open <code>src/client/MainPage.jsx</code> to edit it.
|
||||
</h3>
|
||||
|
||||
<div className="buttons">
|
||||
<a
|
||||
className="button button-filled"
|
||||
href="https://wasp-lang.dev/docs/tutorials/todo-app"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Take the Tutorial
|
||||
</a>
|
||||
<a
|
||||
className="button button-outline"
|
||||
href="https://discord.com/invite/rzdnErX"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Chat on Discord
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default MainPage
|
@ -0,0 +1,4 @@
|
||||
export default function myClientSetupFunction() {
|
||||
// Do some client setup here.
|
||||
}
|
||||
|
1
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/ext-src/vite-env.d.ts
generated
vendored
Normal file
1
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/ext-src/vite-env.d.ts
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="../../.wasp/out/web-app/node_modules/vite/client" />
|
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
30
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/index.tsx
generated
Normal file
30
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/index.tsx
generated
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
import router from './router'
|
||||
import {
|
||||
initializeQueryClient,
|
||||
queryClientInitialized,
|
||||
} from './queryClient'
|
||||
|
||||
import myClientSetupFunction from './ext-src/myClientSetupCode.js'
|
||||
|
||||
startApp()
|
||||
|
||||
async function startApp() {
|
||||
await myClientSetupFunction()
|
||||
initializeQueryClient()
|
||||
|
||||
await render()
|
||||
}
|
||||
|
||||
async function render() {
|
||||
const queryClient = await queryClientInitialized
|
||||
ReactDOM.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{router}
|
||||
</QueryClientProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user