From f5fe865193e3ff1c0614ce20ee1604262099a063 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Wed, 26 Aug 2020 17:02:23 +0200 Subject: [PATCH] Implemented simple Query generator on both FE and BE. --- .../templates/react-app/package.json | 1 + .../templates/react-app/src/config.js | 6 ++ .../templates/react-app/src/queries/_query.js | 21 +++++ .../templates/server/src/routes/index.js | 4 + .../server/src/routes/queries/_query.js | 23 +++++ .../server/src/routes/queries/index.js | 14 ++++ .../Generator/templates/server/src/utils.js | 15 ++++ waspc/src/Generator/ServerGenerator.hs | 12 ++- .../ServerGenerator/QueryGenerator.hs | 84 +++++++++++++++++++ waspc/src/Generator/WebAppGenerator.hs | 13 +-- .../WebAppGenerator/QueryGenerator.hs | 38 +++++++++ 11 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 waspc/data/Generator/templates/react-app/src/config.js create mode 100644 waspc/data/Generator/templates/react-app/src/queries/_query.js create mode 100644 waspc/data/Generator/templates/server/src/routes/queries/_query.js create mode 100644 waspc/data/Generator/templates/server/src/routes/queries/index.js create mode 100644 waspc/data/Generator/templates/server/src/utils.js create mode 100644 waspc/src/Generator/ServerGenerator/QueryGenerator.hs create mode 100644 waspc/src/Generator/WebAppGenerator/QueryGenerator.hs diff --git a/waspc/data/Generator/templates/react-app/package.json b/waspc/data/Generator/templates/react-app/package.json index 27645e4cf..4fb1e6db6 100644 --- a/waspc/data/Generator/templates/react-app/package.json +++ b/waspc/data/Generator/templates/react-app/package.json @@ -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", diff --git a/waspc/data/Generator/templates/react-app/src/config.js b/waspc/data/Generator/templates/react-app/src/config.js new file mode 100644 index 000000000..75d6ccfde --- /dev/null +++ b/waspc/data/Generator/templates/react-app/src/config.js @@ -0,0 +1,6 @@ + +const config = { + apiUrl: 'https://localhost:3001' +} + +export default config diff --git a/waspc/data/Generator/templates/react-app/src/queries/_query.js b/waspc/data/Generator/templates/react-app/src/queries/_query.js new file mode 100644 index 000000000..be390d83a --- /dev/null +++ b/waspc/data/Generator/templates/react-app/src/queries/_query.js @@ -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 =} diff --git a/waspc/data/Generator/templates/server/src/routes/index.js b/waspc/data/Generator/templates/server/src/routes/index.js index 4ecfc60a8..fe53c9417 100644 --- a/waspc/data/Generator/templates/server/src/routes/index.js +++ b/waspc/data/Generator/templates/server/src/routes/index.js @@ -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 diff --git a/waspc/data/Generator/templates/server/src/routes/queries/_query.js b/waspc/data/Generator/templates/server/src/routes/queries/_query.js new file mode 100644 index 000000000..2da07557c --- /dev/null +++ b/waspc/data/Generator/templates/server/src/routes/queries/_query.js @@ -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 }) +}) + diff --git a/waspc/data/Generator/templates/server/src/routes/queries/index.js b/waspc/data/Generator/templates/server/src/routes/queries/index.js new file mode 100644 index 000000000..46439208f --- /dev/null +++ b/waspc/data/Generator/templates/server/src/routes/queries/index.js @@ -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 diff --git a/waspc/data/Generator/templates/server/src/utils.js b/waspc/data/Generator/templates/server/src/utils.js new file mode 100644 index 000000000..c6e0049a7 --- /dev/null +++ b/waspc/data/Generator/templates/server/src/utils.js @@ -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) + } +} diff --git a/waspc/src/Generator/ServerGenerator.hs b/waspc/src/Generator/ServerGenerator.hs index 832a0bf70..5c29e1b52 100644 --- a/waspc/src/Generator/ServerGenerator.hs +++ b/waspc/src/Generator/ServerGenerator.hs @@ -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" diff --git a/waspc/src/Generator/ServerGenerator/QueryGenerator.hs b/waspc/src/Generator/ServerGenerator/QueryGenerator.hs new file mode 100644 index 000000000..7259c0317 --- /dev/null +++ b/waspc/src/Generator/ServerGenerator/QueryGenerator.hs @@ -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 diff --git a/waspc/src/Generator/WebAppGenerator.hs b/waspc/src/Generator/WebAppGenerator.hs index a5ab6fbae..9e6b92cee 100644 --- a/waspc/src/Generator/WebAppGenerator.hs +++ b/waspc/src/Generator/WebAppGenerator.hs @@ -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)) ] diff --git a/waspc/src/Generator/WebAppGenerator/QueryGenerator.hs b/waspc/src/Generator/WebAppGenerator/QueryGenerator.hs new file mode 100644 index 000000000..ed6d09cb0 --- /dev/null +++ b/waspc/src/Generator/WebAppGenerator/QueryGenerator.hs @@ -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) + ] +