Implemented simple Query generator on both FE and BE.

This commit is contained in:
Martin Sosic 2020-08-26 17:02:23 +02:00 committed by Martin Šošić
parent cf0208315f
commit f5fe865193
11 changed files with 225 additions and 6 deletions

View File

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

View File

@ -0,0 +1,6 @@
const config = {
apiUrl: 'https://localhost:3001'
}
export default config

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

View File

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

View File

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

View File

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

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

View File

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

View 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

View File

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

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