mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 19:29:17 +03:00
Implemented simple Query generator on both FE and BE.
This commit is contained in:
parent
cf0208315f
commit
f5fe865193
@ -6,6 +6,7 @@
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@reduxjs/toolkit": "^1.2.3",
|
||||
"axios": "^0.20.0",
|
||||
"lodash": "^4.17.15",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
|
6
waspc/data/Generator/templates/react-app/src/config.js
vendored
Normal file
6
waspc/data/Generator/templates/react-app/src/config.js
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
const config = {
|
||||
apiUrl: 'https://localhost:3001'
|
||||
}
|
||||
|
||||
export default config
|
21
waspc/data/Generator/templates/react-app/src/queries/_query.js
vendored
Normal file
21
waspc/data/Generator/templates/react-app/src/queries/_query.js
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{{={= =}=}}
|
||||
import axios from 'axios'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
const {= queryFnName =} = async ({ args, context }) => {
|
||||
try {
|
||||
const response = await axios.post(config.apiUrl + '/{= queryRoute =}', { args })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
// TODO: This is a really crude error handling for now, and we should look into improving it,
|
||||
// once we figure out what we need. We should start from the server side probably.
|
||||
const e = new Error(error.message)
|
||||
if (error?.response?.data) {
|
||||
e.data = error.response.data
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export default {= queryFnName =}
|
@ -1,4 +1,6 @@
|
||||
{{={= =}=}}
|
||||
import express from 'express'
|
||||
import queries from './queries/index.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@ -6,4 +8,6 @@ router.get('/', function (req, res, next) {
|
||||
res.json('Hello world')
|
||||
})
|
||||
|
||||
router.use('/{= queriesRouteInRootRouter =}', queries)
|
||||
|
||||
export default router
|
||||
|
@ -0,0 +1,23 @@
|
||||
{{={= =}=}}
|
||||
import { handleRejection } from '../../utils.js'
|
||||
{=& queryJsFnImportStatement =}
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
// TODO: We are letting default error handler handle errors, which returns all errors as 500.
|
||||
// We should look into improving this, allowing users to return more information via errors.
|
||||
// Important thing to think about is that not all errors from server can be forwarded to client
|
||||
// because they could be exposing sensitive data, users should always be explicit about which errors
|
||||
// can be exposed to the client, and all other errors should be 500.
|
||||
// But let's first see in practice what we need from errors.
|
||||
|
||||
// TODO: When generating express route for query, generated code would be most human-like if we
|
||||
// generated GET route that uses query arguments.
|
||||
// However, for that, we need to know the types of the arguments so we can cast/parse them.
|
||||
// Also, there is limit on URI length, which could be problem if users want to send some bigger
|
||||
// JSON objects or smth.
|
||||
// So for now we are just going with POST that has JSON in the body -> generated code is not
|
||||
// as human-like as it should be though.
|
||||
const result = await {= queryJsFnIdentifier =}({ args: req.body || {}, context: {} })
|
||||
res.json({ result })
|
||||
})
|
||||
|
@ -0,0 +1,14 @@
|
||||
{{={= =}=}}
|
||||
import express from 'express'
|
||||
|
||||
{=# queryRoutes =}
|
||||
import {= importIdentifier =} from '{= importPath =}'
|
||||
{=/ queryRoutes =}
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
{=# queryRoutes =}
|
||||
router.post('{= routePath =}', {= importIdentifier =})
|
||||
{=/ queryRoutes =}
|
||||
|
||||
export default router
|
15
waspc/data/Generator/templates/server/src/utils.js
Normal file
15
waspc/data/Generator/templates/server/src/utils.js
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
module Generator.ServerGenerator
|
||||
( genServer
|
||||
, queriesRouteInRootRouter
|
||||
) where
|
||||
|
||||
import qualified Path as P
|
||||
import Data.Aeson ((.=), object)
|
||||
|
||||
import StrongPath (Path, Rel, File)
|
||||
import qualified StrongPath as SP
|
||||
@ -10,6 +12,7 @@ import Wasp (Wasp)
|
||||
import CompileOptions (CompileOptions)
|
||||
import Generator.FileDraft (FileDraft)
|
||||
import Generator.ExternalCodeGenerator (generateExternalCodeDir)
|
||||
import Generator.ServerGenerator.QueryGenerator (genQueries)
|
||||
import Generator.ServerGenerator.Common (asTmplFile, asServerFile)
|
||||
import qualified Generator.ServerGenerator.Common as C
|
||||
import qualified Generator.ServerGenerator.ExternalCodeGenerator as ServerExternalCodeGenerator
|
||||
@ -54,16 +57,23 @@ genSrcDir :: Wasp -> [FileDraft]
|
||||
genSrcDir wasp = concat
|
||||
[ [C.copySrcTmplAsIs $ asTmplSrcFile [P.relfile|app.js|]]
|
||||
, [C.copySrcTmplAsIs $ asTmplSrcFile [P.relfile|server.js|]]
|
||||
, [C.copySrcTmplAsIs $ asTmplSrcFile [P.relfile|utils.js|]]
|
||||
, genRoutesDir wasp
|
||||
, genQueries wasp
|
||||
]
|
||||
|
||||
genRoutesDir :: Wasp -> [FileDraft]
|
||||
genRoutesDir _ =
|
||||
-- TODO(martin): We will probably want to extract "routes" path here same as we did with "src", to avoid hardcoding,
|
||||
-- but I did not bother with it yet since it is used only here for now.
|
||||
[ C.copySrcTmplAsIs $ asTmplSrcFile [P.relfile|routes/index.js|]
|
||||
[ C.makeTemplateFD
|
||||
(asTmplFile [P.relfile|src/routes/index.js|])
|
||||
(asServerFile [P.relfile|src/routes/index.js|])
|
||||
(Just $ object [ "queriesRouteInRootRouter" .= queriesRouteInRootRouter ])
|
||||
]
|
||||
|
||||
queriesRouteInRootRouter :: String
|
||||
queriesRouteInRootRouter = "queries"
|
||||
|
||||
|
||||
|
||||
|
84
waspc/src/Generator/ServerGenerator/QueryGenerator.hs
Normal file
84
waspc/src/Generator/ServerGenerator/QueryGenerator.hs
Normal file
@ -0,0 +1,84 @@
|
||||
module Generator.ServerGenerator.QueryGenerator
|
||||
( genQueries
|
||||
, queryRouteInQueriesRouter
|
||||
) where
|
||||
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Aeson ((.=), object)
|
||||
import qualified Path as P
|
||||
|
||||
import qualified Util as U
|
||||
import StrongPath (Path, Rel, File, Dir, (</>))
|
||||
import qualified StrongPath as SP
|
||||
import Wasp (Wasp)
|
||||
import qualified Wasp
|
||||
import qualified Wasp.Query
|
||||
import qualified Wasp.JsImport
|
||||
import Generator.FileDraft (FileDraft)
|
||||
import qualified Generator.ServerGenerator.Common as C
|
||||
|
||||
genQueries :: Wasp -> [FileDraft]
|
||||
genQueries = genQueryRoutes
|
||||
|
||||
genQueryRoutes :: Wasp -> [FileDraft]
|
||||
genQueryRoutes wasp = concat
|
||||
[ map (genQueryRoute wasp) (Wasp.getQueries wasp)
|
||||
, [genQueriesRouter wasp]
|
||||
]
|
||||
|
||||
genQueryRoute :: Wasp -> Wasp.Query.Query -> FileDraft
|
||||
genQueryRoute _ query = C.makeTemplateFD tmplFile dstFile (Just tmplData)
|
||||
where
|
||||
tmplFile = C.asTmplFile [P.relfile|src/routes/queries/_query.js|]
|
||||
|
||||
dstFile = queryRoutesDirInServerRootDir </> queryRouteFileInQueryRoutesDir query
|
||||
|
||||
tmplData = object
|
||||
[ "queryJsFnImportStatement" .= ("import " ++ importWhat ++ " from '" ++ fromPath ++ "'")
|
||||
, "queryJsFnIdentifier" .= importIdentifier
|
||||
]
|
||||
|
||||
jsQueryImport = Wasp.Query._jsFunction query
|
||||
|
||||
(importIdentifier, importWhat) =
|
||||
case (Wasp.JsImport._defaultImport jsQueryImport, Wasp.JsImport._namedImports jsQueryImport) of
|
||||
(Just defaultImport, []) -> (defaultImport, defaultImport)
|
||||
(Nothing, [namedImport]) -> (namedImport, "{ " ++ namedImport ++ " }")
|
||||
_ -> error "Expected either default import or single named import for query js function."
|
||||
|
||||
fromPath = relPathToExtSrcDir ++ SP.toFilePath (Wasp.JsImport._from jsQueryImport)
|
||||
|
||||
data QueryRoutesDir
|
||||
|
||||
queryRoutesDirInServerSrcDir :: Path (Rel C.ServerSrcDir) (Dir QueryRoutesDir)
|
||||
queryRoutesDirInServerSrcDir = SP.fromPathRelDir [P.reldir|routes/queries/|]
|
||||
|
||||
queryRoutesDirInServerRootDir :: Path (Rel C.ServerRootDir) (Dir QueryRoutesDir)
|
||||
queryRoutesDirInServerRootDir = C.serverSrcDirInServerRootDir </> queryRoutesDirInServerSrcDir
|
||||
|
||||
-- | TODO: fromJust here could fail if query name is weird, we should handle that.
|
||||
queryRouteFileInQueryRoutesDir :: Wasp.Query.Query -> Path (Rel QueryRoutesDir) File
|
||||
queryRouteFileInQueryRoutesDir query = fromJust $ SP.parseRelFile $ Wasp.Query._name query ++ ".js"
|
||||
|
||||
-- | TODO: Make this not hardcoded! Maybe even use StrongPath? But I can't because of ../../ .
|
||||
relPathToExtSrcDir :: FilePath
|
||||
relPathToExtSrcDir = "../../ext-src/"
|
||||
|
||||
genQueriesRouter :: Wasp -> FileDraft
|
||||
genQueriesRouter wasp = C.makeTemplateFD tmplFile dstFile (Just tmplData)
|
||||
where
|
||||
tmplFile = C.asTmplFile [P.relfile|src/routes/queries/index.js|]
|
||||
dstFile = queryRoutesDirInServerRootDir </> SP.fromPathRelFile [P.relfile|index.js|]
|
||||
tmplData = object
|
||||
[ "queryRoutes" .= map makeQueryRoute (Wasp.getQueries wasp)
|
||||
]
|
||||
makeQueryRoute query =
|
||||
let queryName = Wasp.Query._name query
|
||||
in object
|
||||
[ "importIdentifier" .= queryName
|
||||
, "importPath" .= ("./" ++ SP.toFilePath (queryRouteFileInQueryRoutesDir query))
|
||||
, "routePath" .= ("/" ++ queryRouteInQueriesRouter query)
|
||||
]
|
||||
|
||||
queryRouteInQueriesRouter :: Wasp.Query.Query -> String
|
||||
queryRouteInQueriesRouter = U.camelToKebabCase . Wasp.Query._name
|
@ -18,6 +18,7 @@ import qualified Generator.WebAppGenerator.ButtonGenerator as ButtonGenerator
|
||||
import Generator.WebAppGenerator.Common (asTmplFile, asWebAppFile, asWebAppSrcFile)
|
||||
import qualified Generator.WebAppGenerator.Common as C
|
||||
import qualified Generator.WebAppGenerator.ExternalCodeGenerator as WebAppExternalCodeGenerator
|
||||
import Generator.WebAppGenerator.QueryGenerator (genQueries)
|
||||
|
||||
|
||||
generateWebApp :: Wasp -> CompileOptions -> [FileDraft]
|
||||
@ -65,29 +66,31 @@ generateSrcDir wasp
|
||||
, [P.relfile|serviceWorker.js|]
|
||||
, [P.relfile|store/index.js|]
|
||||
, [P.relfile|store/middleware/logger.js|]
|
||||
, [P.relfile|config.js|]
|
||||
]
|
||||
++ EntityGenerator.generateEntities wasp
|
||||
++ ButtonGenerator.generateButtons wasp
|
||||
++ [generateReducersJs wasp]
|
||||
++ genQueries wasp
|
||||
where
|
||||
generateLogo = C.makeTemplateFD (asTmplFile [P.relfile|src/logo.png|])
|
||||
(srcDir </> (asWebAppSrcFile [P.relfile|logo.png|]))
|
||||
(srcDir </> asWebAppSrcFile [P.relfile|logo.png|])
|
||||
Nothing
|
||||
makeSimpleSrcTemplateFD path = C.makeTemplateFD (asTmplFile $ [P.reldir|src|] P.</> path)
|
||||
(srcDir </> (asWebAppSrcFile path))
|
||||
(srcDir </> asWebAppSrcFile path)
|
||||
(Just $ toJSON wasp)
|
||||
|
||||
generateReducersJs :: Wasp -> FileDraft
|
||||
generateReducersJs wasp = C.makeTemplateFD tmplPath dstPath (Just templateData)
|
||||
where
|
||||
tmplPath = asTmplFile [P.relfile|src/reducers.js|]
|
||||
dstPath = srcDir </> (asWebAppSrcFile [P.relfile|reducers.js|])
|
||||
dstPath = srcDir </> asWebAppSrcFile [P.relfile|reducers.js|]
|
||||
templateData = object
|
||||
[ "wasp" .= wasp
|
||||
, "entities" .= map toEntityData (getEntities wasp)
|
||||
]
|
||||
toEntityData entity = object
|
||||
[ "entity" .= entity
|
||||
, "entityLowerName" .= (Util.toLowerFirst $ entityName entity)
|
||||
, "entityStatePath" .= ("./" ++ (SP.toFilePath $ EntityGenerator.entityStatePathInSrc entity))
|
||||
, "entityLowerName" .= Util.toLowerFirst (entityName entity)
|
||||
, "entityStatePath" .= ("./" ++ SP.toFilePath (EntityGenerator.entityStatePathInSrc entity))
|
||||
]
|
||||
|
38
waspc/src/Generator/WebAppGenerator/QueryGenerator.hs
Normal file
38
waspc/src/Generator/WebAppGenerator/QueryGenerator.hs
Normal file
@ -0,0 +1,38 @@
|
||||
module Generator.WebAppGenerator.QueryGenerator
|
||||
( genQueries
|
||||
) where
|
||||
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Aeson ((.=), object)
|
||||
import qualified Path as P
|
||||
|
||||
import Wasp (Wasp)
|
||||
import qualified Wasp
|
||||
import qualified Wasp.Query
|
||||
import Generator.FileDraft (FileDraft)
|
||||
import qualified Generator.ServerGenerator as ServerGenerator
|
||||
import qualified Generator.ServerGenerator.QueryGenerator as ServerGenerator.QueryGenerator
|
||||
import qualified Generator.WebAppGenerator.Common as C
|
||||
|
||||
genQueries :: Wasp -> [FileDraft]
|
||||
genQueries wasp = concat
|
||||
[ map (genQuery wasp) (Wasp.getQueries wasp)
|
||||
]
|
||||
|
||||
genQuery :: Wasp -> Wasp.Query.Query -> FileDraft
|
||||
genQuery _ query = C.makeTemplateFD tmplFile dstFile (Just tmplData)
|
||||
where
|
||||
tmplFile = C.asTmplFile [P.relfile|src/queries/_query.js|]
|
||||
|
||||
-- | TODO: fromJust here could fail if there is some problem with the name, we should handle this.
|
||||
dstFile = C.asWebAppFile $ [P.reldir|src/queries/|] P.</> fromJust (P.parseRelFile dstFileName)
|
||||
|
||||
dstFileName = Wasp.Query._name query ++ ".js"
|
||||
|
||||
tmplData = object
|
||||
[ "queryFnName" .= Wasp.Query._name query
|
||||
, "queryRoute" .=
|
||||
(ServerGenerator.queriesRouteInRootRouter
|
||||
++ "/" ++ ServerGenerator.QueryGenerator.queryRouteInQueriesRouter query)
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user