The full code for this project can be found here: https://github.com/AlexMercedCoder/KoaStarterBlog
The Video series I was making with this can be found here so you can see how things worked at different stages: https://www.youtube.com/watch?v=8_aWw7lfKKI&list=PLY6oTPmKnKbbF4t0Y9DcUVYi7f4kix7Qj
I actually illustrate the problem in the beginning part of this video and walk through the construction of all my routes: https://youtu.be/ltAxokJsaWE
So I built out this basic blog app using KoaJS. When I initially run the index.js the behavior is as follows.
- Root Route Works
- Create route works
- Admin route works
- The Delete Button works on the admin page
- The edit route just doesn't not (works on and off even though the code is just like the other routes)
Bigger Problem: After submitting a form by hitting the delete button or by creating a new post all the routes except the create route stop working and instead they just return (not found). At first I thought this was a problem being caused by ctx.redirect because they would always fail and be followed by the broken routes but while rendering a complete page seems to work initially typing root or admin route into the browser after form submission still breaks.
*Update: this happens after going to any route, every route works if its the first route accessed but then all other routes except create stop working afterwards. It's as if the first route creates some sort of limbo. The weird thing is the router still console logs everything the route should do up until either a ctx.render, ctx.redirect or ctx.body is to be returned.
below is the index.js code!
///////////////////////
//Initializing Environment Variables and other middleware
//npm i dotenv
//npm i koa-methodoverride
///////////////////////
require('dotenv').config();
const override = require('koa-methodoverride');
const parser = require('koa-bodyparser');
////////////////////////
//Connecting the DB
//npm i mongoose
////////////////////////
const mongoose = require('mongoose');
const db = mongoose.connection;
const host = process.env.host;
const dbupdate = {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false};
mongoose.connect(host, dbupdate);
db.on('error', (err) => console.log('Error, DB Not connected'));
db.on('connected', () => console.log ('connected to mongo'));
db.on('diconnected', () => console.log ('Mongo is disconnected'));
db.on('open', () =>console.log ('Connection Made!'));
////////////////////////////
//Model Schema
///////////////////////////
const Blog = require('./model/blog.js');
///////////////////////
//Create Our Server Object
//npm i koa
///////////////////////
const koa = require('koa');
const server = new koa();
//////////////////////////
//Create Our Static Folder
//npm i koa-static
//////////////////////////
const static = require('koa-static');
//////////////////////////
//Creating Our Router
//npm i koa-router
//////////////////////////
const Router = require('koa-router');
const route = new Router();
/////////////////////////////////
//initializing views
//npm i koa-views
//npm i nunjucks
////////////////////////////////;
const views = require('koa-views');
const nunj = require('nunjucks');
nunj.configure('./views', {autoescape: true});
///////////////////////////
//routes
// route.get - route.post - route.patch - post.put - route.delete
///////////////////////////
//root route
route.get('/', (ctx, next) => {
console.log('connected to root route');
return Blog.find({}, (error, results) => {
console.log(results)
ctx.render('index.njk', {
posts: results
});
});
});
//admin route
route.get('/admin', (ctx, next) => {
console.log('connected to admin route');
return Blog.find({}, (error, results) => {
console.log(results)
ctx.render('admin.njk', {
posts: results
});
});
});
//delete route
route.delete('/delete/:id', (ctx, next) => {
console.log('connected to delete route');
console.log(ctx.request.body)
if (ctx.request.body.pw === process.env.pw){
Blog.findByIdAndRemove(ctx.params.id, (err, result) => {
})
}else{
console.log('wrong password')
}
return ctx.render('complete.njk');
});
//edit route
route.get('/edit/:id', (ctx, next) => {
console.log('connected to edit route');
return Blog.findById(ctx.params.id, (err, results) => {
console.log(results);
ctx.render('edit.njk', {
post: results
});
});
});
route.put('/edit/:id', (ctx, next) => {
console.log('editing a post');
console.log(ctx.request.body)
if (ctx.request.body.pw === process.env.pw){
Blog.findByIdAndUpdate(ctx.params.id, ctx.request.body, {new:True}, (err, result) => {
console.log(result);
})
}else{
console.log('wrong password');
}
return ctx.render('complete.njk');
});
//create route
route.get('/create', (ctx, next) => {
console.log('connected to create route');
return ctx.render('create.njk');
});
route.post('/create', (ctx, next) => {
console.log('creating a post');
console.log(ctx.request.body)
if (ctx.request.body.pw === process.env.pw){
Blog.create(ctx.request.body, (err, result) => {
console.log(result);
})
}else{
console.log('wrong password');
;
}
return ctx.render('complete.njk');
});
////////////////////////////
//Async Functions
////////////////////////////
// const getPosts = async (query) => {
// const data = await Blog.find({query})
// return data;
// };
////////////////////////
//Middleware
/////////////////////////
server.use(parser());
server.use(override('_method'))
server.use(views('./views', {map: {njk: 'nunjucks'}}));
server.use(route.routes());
server.use(static('./public'));
/////////////////////
//Our Listener on Port 1985
/////////////////////
server.listen(1985,'localhost',() => console.log('Listening on port 1985'));
It looks like a http-server fault. Try to add an error handler.
Also I recomment to change error handling in code e.g.
route.put('/edit/:id', (ctx, next) => {
if (ctx.request.body.pw === process.env.pw){
Blog.findByIdAndUpdate(ctx.params.id, ctx.request.body, {new:True}, (err, result) => {
console.log(result);
})
} else {
console.log('wrong password');
}
return ctx.render('complete.njk');
});
replace by
route.put('/edit/:id', (ctx, next) => {
// Early stop to avoid brace ladder
if (ctx.request.body.pw != process.env.pw)
ctx.throw('Env. wrong password'); // Pass request to error-handler
Blog.findByIdAndUpdate(ctx.params.id, ctx.request.body, {new:True}, (err, result) => {
if (err)
ctx.throw('Db wrong password'); // or throw Error('Db wrong password');
ctx.render('complete.njk');
});
}
...
server.use(route.routes());
server.use(static('./public'));
// Error handler: catch 'wrong password'-error here.
app.on('error', (err, ctx) => {
console.error(err);
ctx.render('error.njk');
});
P.S. I use Express, not Koa. So maybe I made some mistakes.
Perhaps ctx.render requires async-await framing like below
route.get('/', async (ctx, next) => {
console.log('connected to root route');
console.log(ctx);
return Blog.find({}, (error, results) => {
console.log(results)
await ctx.render('index', {
posts: results
});
console.log('the view was rendered')
});
});
I downloaded your git and made this changes. And it works :)
I never figured out was causing this behavior but I was able to fix and committed the working code to the github repository. After several attempts I could not get the koa-views library to work right so I gave up on it and switched to koa-nunjucks-2 and initialized it exactly as it says in their docs.
Unfortunately... the same problem kept happening till I changed the path to relative path with using path.join then koa-nunjucks-2 worked perfectly!
Why, or what was happening, I still have no idea. It probably has to do with unresolved promises under the hood but I'm not really sure.
Related
I have an asynchronous function that is called when the api is connected to. this should return some json and then it will be displayed on the json response of the page. In the json response I get undefined.
This is the code i am using:
const express = require('express');
const router = express.Router();
const superagent = require('superagent');
function getCyrpto(){
var result;
superagent.get('https://min-api.cryptocompare.com/data/v2/pair/mapping/exchange?e=Kraken')
.query({ api_key: 'xxxxxxxx'})
.end((err, res) => {
if (err) { return console.log(err); }
result = res.body;
});
setTimeout(() => {
console.log(result);
return result;
}, 2000)
}
router.get('/', (req, res, next) => {
crypto=getCyrpto()
setTimeout(()=> {
res.status(200).json({
message: 'geting cyrpto',
apiResponse: crypto
});
}, 2500)
});
The reason it is happeing because your setTimeOut methods runs before your api call get the result and assign it to the result.
This is a common problem most of us face when we start to learn concurrency concept.
For example:
console.log("a");
setTimeOut(()=>console.log("b"),1000);
console.log("c");
Output of above function will
a
c
b
this is happening beacause setTimeout function is a promise which means your nodejs will not wait for it to finish before running the next line, it will just process the setTimeout function in background and when it will finish it will call its callback function given as first parameter in setTimeOut.
Your solution should be
function getCyrpto(){
return new Promise((resolve,reject)=>{
var result;
superagent.get('https://min-api.cryptocompare.com/data/v2/pair/mapping/exchange?e=Kraken')
.query({ api_key: 'xxxxxxxx'})
.end((err, res) => {
if (err) { console.log(err); reject(err); }
result = res.body;
setTimeout(() => {
console.log(result);
resolve(result);
}, 2000)
});
}
router.get('/', (req, res, next) => {
getCyrpto().then(crypto=>{
setTimeout(()=> {
res.status(200).json({
message: 'geting cyrpto',
apiResponse: crypto
},2500);
}).catch(err=>{res.status(400).json(err)})
}
I run node app with CORS.
Currently I load whitelisted domains from .env file but I need to change it to load them from database. The whole CORS functionality is synchronous and I wonder what's the proper way to make db query for it.
app.js
app.use(cors());
async function listenCallback(server) {
try {
// app code
} catch (err) {
server.close();
}
cors.js
const cors = require('cors');
const db = require('../db');
const whitelist = db.raw('select domains from table'); // I need to change this
module.exports = (enabled = true) =>
(req, res, next) => {
const options = {
origin(origin, callback) {
if (!origin) {
callback(null, true);
return;
}
const originIsWhitelisted = enabled ? whitelist.indexOf(origin) !== -1 : true;
if (originIsWhitelisted) {
callback(null, originIsWhitelisted);
return;
}
callback({
statusCode: 401,
error: 'Not allowed',
});
},
};
return cors(options)(req, res, next);
};
If what you're trying to do is to load a set of domains from a database upon module initialization so you can then use those in some middleware, then you can get a promise from loading the whilelist from the database and then just use that promise in your middleware.
const cors = require('cors');
const db = require('../db');
const whitelistPromise = db.someOperationThatReturnsPromise();
module.exports = (enabled = true) =>
(req, res, next) => {
const options = {
origin(origin, callback) {
if (!origin) {
callback(null, true);
return;
}
whitelistPromise.then(whitelist => {
const originIsWhitelisted = enabled ? whitelist.indexOf(origin) !== -1 : true;
if (originIsWhitelisted) {
callback(null, originIsWhitelisted);
return;
}
callback({
statusCode: 401,
error: 'Not allowed',
});
}).catch(err => {
console.log(err);
next(err);
return;
});
},
};
return cors(options)(req, res, next);
};
Another approach is to export a module constructor function from the module that returns a promise that tells your server when your module initialization is done and provides this middleware function as the resolved value. I like this approach better because if the db operation fails, you will know it more centrally and can provide better error handling.
const cors = require('cors');
const db = require('../db');
// export module constructor that initializes this module asynchronously and returns a promise
// That promise resolves to the middleware function (so it can't be used before
// the module is properly initialized)
module.exports = function() {
return db.someOperationThatReturnsPromise().then(whitelist => {
return (enabled = true) => (req, res, next) => {
const options = {
origin(origin, callback) {
if (!origin) {
callback(null, true);
return;
}
const originIsWhitelisted = enabled ? whitelist.indexOf(origin) !== -1 : true;
if (originIsWhitelisted) {
callback(null, originIsWhitelisted);
return;
}
callback({
statusCode: 401,
error: 'Not allowed',
});
},
};
return cors(options)(req, res, next);
};
});
}
In this case, whoever loads this module will do it something like this:
require('./myModule')().then(makeWhitelistMiddleware => {
// put route handlers here that use the function to make CORS middleware
// don't start your server until this completes
app.use(makeWhitelistMiddleware());
// other routes here
app.get(...)
// start server
app.listen(...);
}).catch(err => {
console.log("Could not initialize myModule");
process.exit(1);
});
Lastly, I'll add that there is effort underway to allow top level await inside of module initialization where the whole module loader system can be made aware of promises involved in initializing a module. That type of capability would make this a lot simpler, but until then you have to add your own custom code to initialize the module differently and load the module differently.
I'm new to Node.js and am trying to pass some data from my DB model back to the router but I'm unable to find a solution. I have the following route file that makes a call to model:
Route file:
var express = require('express');
var router = express.Router();
var db = require('../db');
var customers = require('../models/customers');
db.connect(function(err) {
if (err) {
console.log('Unable to connect to MySQL.')
process.exit(1)
}
});
router.post('/', function(req, res) {
customers.checkPassword(req.body.cust_id, req.body.password);
res.sendStatus(200);
});
Model file:
var db = require('../db.js');
module.exports.checkPassword = function(cust_id, password) {
var sql = "SELECT Password FROM Shop.customers WHERE ID =" + cust_id;
db.get().query(sql, function (err, res, fields) {
result = res[0].Password;
if (err) throw err
});
};
My question is: how could I pass the queried result Password back to my Route file so that I can do this:
console.log('Password is', result);
I appreciate any help on this.
I'd use a promise
Model file
module.exports.checkPassword = function(cust_id, password) {
return new Promise(function(resolve, reject) {
const sql = "SELECT Password FROM Shop.customers WHERE ID =" + cust_id;
db.get().query(sql, function (err, res, fields) {
if (err) return reject(err)
result = res[0].Password;
return resolve(result);
});
});
};
Route file
var express = require('express');
var router = express.Router();
var db = require('../db');
var customers = require('../models/customers');
db.connect(function(err) {
if (err) {
console.log('Unable to connect to MySQL.')
process.exit(1)
}
});
router.post('/', function(req, res) {
customers.checkPassword(req.body.cust_id, req.body.password)
.then((result) => {
// DO: something with result
res.status(200).send();
})
.catch(console.log); // TODO: Handle errors
});
With async/await
router.post('/', async function(req, res) {
try {
const result = await customers.checkPassword(req.body.cust_id, req.body.password)
// DO: something with the result
} catch (e) {
console.log(e); // TODO: handle errors
} finally {
res.status(200).send();
}
});
I assume console.log('Password is', result); is just for test prupose, obviously you should never log a password! Also I suggest to move the callbabck of the routes do a different module, to improve code redability.
You might also find useful promise-module module on npm, basically a promise wrapper around mysql.
You can delegate the credential control to another function in your DB file where you can decide on what kind of data you want to return on success and failure to find such data. Then you can access it from where you are calling it.
I want to add custom Express route using boot script.
I want to check if the apiKey which is in the query string exists in our model.
So, I want to access the model.
But, it seems that the script doesnt get executed since I don't get the model (maybe due to the asynchronous thing).
So, How to do this?
P.S. this is my code
app.post('/webhook', line.middleware(config), (req, res) => {
if (req.query.apiKey) {
const Store = app.models.Store;
Store.find({where: {apiKey: req.query.apiKey}, limit: 1}, function(err, store) {
if (err || store == null) {
res.status(401).end();
}
Promise
.all(req.body.events.map(handleEvent))
.then((result) => res.json(result))
.catch((err) => {
console.error(err);
res.status(500).end();
});
});
}
// server/boot/routes.js
const bodyParser = require('body-parser')
module.exports = function (app) {
const router = app.loopback.Router()
router.post('/webhook', bodyParser.raw({type: '*/*'}), function(req, res) {
// here you have full access to your models
app.models
}
}
I have myRoute.js with a route (GET) defined and I want to call an api endpoint from another route (api.js), and I'm not sure what the right way to do this is. The api.js route is working properly (image and code below).
api.js
router.get('/getGroups/:uid', function(req, res, next) {
let uid = req.params.uid;
db.getAllGroups(uid).then((data) => {
let response =[];
for (i in data) {
response.push(data[i].groupname);
}
res.status(200).send(response);
})
.catch(function (err) {
return err;
});
});
works as expected:
myRoute.js
I would like when a user goes to localhost:3000/USER_ID that the route definition gets information from the api. Psuedo code below (someFunction).
router.get('/:uid', function(req, res, next) {
let uid = req.params.uid;
let fromApi = someFunction(`localhost:3000/getAllGroups/${uid}`); // <--!!!
console.log(fromApi) ; //expecting array
res.render('./personal/index.jade', {fromApi JSON stringified});
});
Not sure if i understand you correct but anyway i will try to help. So you have an api like
router.get('/getGroups/:uid', function(req, res, next) {
let uid = req.params.uid;
db.getAllGroups(uid).then((data) => {
let response =[];
for (i in data) {
response.push(data[i].groupname);
}
res.status(200).send(response);
})
.catch(function (err) {
return err;
});
});
If you would like to reuse it you can extract a function from the code above like so:
async function getAllGroupsByUserId(uid){
const result = [];
try{
const data = await db.getAllGroups(uid);
for (i in data) {
result.push(data[i].groupname);
};
return result;
}
catch(e) {
return e;
}
}
And then reuse it in your api & anywhere you want:
router.get('/getGroups/:uid', async function(req, res, next) {
const uid = req.params.uid;
const groups = await getAllGroupsByUserId(uid);
res.status(200).send(groups);
})
Same you can do in your another route:
router.get('/:uid', async function(req, res, next) {
const uid = req.params.uid;
const fromApi = await getAllGroupsByUserId(uid); // <--!!!
console.log(fromApi) ; //expecting array
res.render('./personal/index.jade', {fromApi JSON stringified});
});
Seems like pretty clear :)
I would use fetch for this. You can replace someFunction with fetch, and then put the res.render code in a .then(). So, you would get this:
const fetch = require("node-fetch");
router.get('/:uid', function(req, res, next) {
let uid = req.params.uid;
fetch('localhost:3000/getAllGroups/${uid}').then(res => res.json()).then(function(data) {
returned = data.json();
console.log(returned); //expecting array
res.render('./personal/index.jade', {JSON.stringify(returned)});
});
});
A more robust way with error handling would be to write something like this:
const fetch = require("node-fetch");
function handleErrors(response) {
if(!response.ok) {
throw new Error("Request failed " + response.statusText);
}
return response;
}
router.get('/:uid', function(req, res, next) {
let uid = req.params.uid;
fetch('localhost:3000/getAllGroups/${uid}')
.then(handleErrors)
.then(res => res.json())
.then(function(data) {
console.log(data) ; //expecting array
res.render('./personal/index.jade', {JSON.stringify(data)});
})
.catch(function(err) {
// handle the error here
})
});
The ideal way would be to abstract your code into a method so you aren't calling yourself, as The Reason said. However, if you really want to call yourself, this will work.