Unable to reassign ctx.query in a koa middleware - javascript

https://codesandbox.io/p/sandbox/test-koa-middleware-forked-2kx1fi?file=%2Fsrc%2Findex.js&selection=%5B%7B%22endColumn%22%3A4%2C%22endLineNumber%22%3A21%2C%22startColumn%22%3A4%2C%22startLineNumber%22%3A21%7D%5D
Here's what I am trying to accomplish. I want to pass in a query string to the endpoint in the code sandbox, validate it against a Joi schema, and then reassign the resulting value from the Joi schema to ctx.query in the koa context. The reason I want to do this, is to 1) validate my endpoint, and 2) gain the benefit of type conversions that come with Joi (i.e. a query string that is passed as a string that can be interpretted as a number should come out as a number. A single entry that can be interpretted as a one element array, will come out as an array not a single entry.
Here's the applicable code
const Koa = require("koa");
const app = new Koa();
const Joi = require("joi");
const PORT = 3000;
const validSchema = Joi.object({
name: Joi.string().optional(),
id: Joi.number().required().min(0),
arr: Joi.array().items(Joi.number()).optional().single(),
});
// #1
app.use(async (ctx, next) => {
console.log("INITIAL QUERY OBJECT", ctx.query);
const { error, value } = validSchema.validate(ctx.query);
if (!!error) ctx.throw(400, error);
console.log("QUERY FROM JOI VALIDATION", value);
value.id += 2;
console.log(typeof value.id);
ctx.query = value;
console.log("CTX.QUERY AFTER ASSIGNMENT", ctx.query);
await next();
});
// #2
app.use(async (ctx, next) => {
console.log("QUERY IN 2ND MIDDLEWARE", ctx.query);
console.log(typeof ctx.query.id);
await next();
});
app.use(async (ctx, next) => {
ctx.body = `test koa`;
});
app.listen(PORT);
console.log(`http://localhost:${PORT}`);
Here is what I get from the result when the query string is "?id=2":
INITIAL QUERY OBJECT [Object: null prototype] {}
INITIAL QUERY OBJECT [Object: null prototype] { id: '2' }
QUERY FROM JOI VALIDATION [Object: null prototype] { id: 2 }
number
CTX.QUERY AFTER ASSIGNMENT [Object: null prototype] { id: '4' }
QUERY IN 2ND MIDDLEWARE [Object: null prototype] { id: '4' }
string
Here is what I expect:
INITIAL QUERY OBJECT [Object: null prototype] {}
INITIAL QUERY OBJECT [Object: null prototype] { id: '2' }
QUERY FROM JOI VALIDATION [Object: null prototype] { id: 2 }
number
CTX.QUERY AFTER ASSIGNMENT [Object: null prototype] { id: 4 }
QUERY IN 2ND MIDDLEWARE [Object: null prototype] { id: 4 }
number
Additionally, if I provide the arr query param, I should expect a single element array out not just a single element (query string '?id=2&arr=5').
What I get:
INITIAL QUERY OBJECT [Object: null prototype] { id: '2', arr: '5' }
QUERY FROM JOI VALIDATION [Object: null prototype] { id: 2, arr: [ 5 ] }
number
CTX.QUERY AFTER ASSIGNMENT [Object: null prototype] { id: '4', arr: '5' }
QUERY IN 2ND MIDDLEWARE [Object: null prototype] { id: '4', arr: '5' }
string
What I want:
INITIAL QUERY OBJECT [Object: null prototype] { id: '2', arr: '5' }
QUERY FROM JOI VALIDATION [Object: null prototype] { id: 2, arr: [ 5 ] }
number
CTX.QUERY AFTER ASSIGNMENT [Object: null prototype] { id: '4', arr: [ 5 ] }
QUERY IN 2ND MIDDLEWARE [Object: null prototype] { id: '4', arr: [ 5 ] }
number
Obviously, one solution is to move the Joi validation out of middleware or reperform it in the handler function for a given route, but that is incredibly wasteful and leads to repeated code where none should be required, especially since koa docs indicate you CAN reassign ctx.query (and indeed you do, it just loses typing).

Related

Unwanted objects added on my mongodb response [duplicate]

I have defined a Model with mongoose like this:
var mongoose = require("mongoose")
var Schema = mongoose.Schema
var userObject = Object.create({
alias: String,
email: String,
password: String,
updated: {
type: Date,
default: Date.now
}
})
var userSchema = new Schema(userObject, {strict: false})
var User = mongoose.model('User', userSchema)
module.exports = User
Then I created a user that I can perfectly find through mongo console like this:
db.users.findOne({ email: "coco#coco.com" });
{
"_id" : ObjectId("55e97420d82ebdea3497afc7"),
"password" : "caff3a46ebe640e5b4175a26f11105bf7e18be76",
"gravatar" : "a4bfba4352aeadf620acb1468337fa49",
"email" : "coco#coco.com",
"alias" : "coco",
"updated" : ISODate("2015-09-04T10:36:16.059Z"),
"apps" : [ ],
"__v" : 0
}
However, when I try to access this object through a node.js with mongoose, the object a retrieve is not such doc, but a wrapper:
This piece of code...
// Find the user for which the login queries
var User = require('../models/User')
User.findOne({ email: mail }, function(err, doc) {
if (err) throw err
if (doc) {
console.dir(doc)
if(doc.password == pass) // Passwords won't match
Produces this output from console.dir(doc)...
{ '$__':
{ strictMode: false,
selected: undefined,
shardval: undefined,
saveError: undefined,
validationError: undefined,
adhocPaths: undefined,
removing: undefined,
inserting: undefined,
version: undefined,
getters: {},
_id: undefined,
populate: undefined,
populated: undefined,
wasPopulated: false,
scope: undefined,
activePaths: { paths: [Object], states: [Object], stateNames: [Object] },
ownerDocument: undefined,
fullPath: undefined,
emitter: { domain: null, _events: {}, _maxListeners: 0 } },
isNew: false,
errors: undefined,
_doc:
{ __v: 0,
apps: [],
updated: Fri Sep 04 2015 12:36:16 GMT+0200 (CEST),
alias: 'coco',
email: 'coco#coco.com',
gravatar: 'a4bfba4352aeadf620acb1468337fa49',
password: 'caff3a46ebe640e5b4175a26f11105bf7e18be76',
_id: { _bsontype: 'ObjectID', id: 'Uét Ø.½ê4¯Ç' } },
'$__original_validate': { [Function] numAsyncPres: 0 },
validate: [Function: wrappedPointCut],
_pres: { '$__original_validate': [ [Object] ] },
_posts: { '$__original_validate': [] } }
Therefore, passwords won't match because doc.password is undefined.
Why is this caused?
That's exactly the purpose of mongoose, wrapping mongo objects. It's what provides the ability to call mongoose methods on your documents. If you'd like the simple object, you can call .toObject() or use a lean query if you don't plan on using any mongoose magic on it at all. That being said, the equality check should still hold as doc.password returns doc._doc.password.

Node JS [Object Map] while writing object to file

I read many posts on stackoverflow and other tutorial sites for writing object/JSON to a file, but none of the solutions worked for me.
1) Code:
let messages = await fetchMessages()
console.log(messages) // Prints an object
fs.writeFileSync('./msgdata.json', messages , 'utf-8');
2) Also tried,
fs.writeFileSync('./msgdata.json', JSON.stringify(messages) , 'utf-8');
3) Also tried,
fs.writeFile(), but get same output as above.
msgdata.json: (For 1)
[object Map]
msgdata.json: (For 2 & 3)
{}
Can someone please point out what could be causing this?
## Output of console.log(messages) ##
OUTPUT contains 10 more such objects having different id's:
channel:
TextChannel {
type: 'text',
id: '424825532274253312',
name: 'request',
position: 69,
parentID: '397363224286473987',
permissionOverwrites: [Object],
topic: null,
nsfw: false,
lastMessageID: '427143105410760704',
guild: [Object],
messages: [Object],
_typing: Map {} },
id: '427142596817846272',
type: 'DEFAULT',
content: '**Select your emoji:** __***Group 9:***__',
author:
ClientUser {
id: '407083773537350272',
username: 'Bot',
discriminator: '9256',
avatar: '9c374e719ba2ab4e69fd577005b635bf',
bot: true,
lastMessageID: null,
lastMessage: null,
verified: true,
email: null,
localPresence: {},
_typing: Map {},
friends: Collection {},
blocked: Collection {},
notes: Collection {},
premium: null,
mfaEnabled: false,
mobile: null,
settings: [Object],
guildSettings: Collection {} },
member:
GuildMember {
guild: [Object],
user: [Object],
_roles: [Array],
serverDeaf: false,
serverMute: false,
selfMute: undefined,
selfDeaf: undefined,
voiceSessionID: undefined,
voiceChannelID: undefined,
speaking: false,
nickname: null,
joinedTimestamp: 1517127343434,
lastMessageID: null,
lastMessage: null },
pinned: false,
tts: false,
nonce: undefined,
system: false,
embeds: [],
attachments: Collection {},
createdTimestamp: 1521909131007,
editedTimestamp: null,
reactions:
Collection {
'taillow:417281639777959940' => [Object],
'shroomish:417281639899463680' => [Object],
'sableye:417281640197521419' => [Object],
'ralts:417281642735075329' => [Object],
'sentret:417281644001624076' => [Object],
'shuppet:417281644291162132' => [Object],
'torchic:417281647210397706' => [Object],
'snubbull:417281647692480522' => [Object],
'sunkern:417281647763783681' => [Object],
'slowpoke:417281648653107200' => [Object],
'teddiursa:417281649537974273' => [Object],
'sneasel:417281649613471747' => [Object],
'snorunt:417281649819123712' => [Object],
'surskit:417281650163056640' => [Object],
'qwilfish:417281654629859348' => [Object],
'shelgon:417281654730522624' => [Object] },
mentions:
MessageMentions {
everyone: false,
users: Collection {},
roles: Collection {},
_content: '**Select your emoji:** __***Group
9:***__',
_client: [Object],
_guild: [Object],
_members: null,
_channels: null },
webhookID: null,
hit: null,
_edits: [] },
This is because the object returned is a Map object (MDN). When you JSON.strigify a Map object, it always returns {}. There are two ways to get readable JSON that stores your map. The first is to loop over all the entries and create a JSON (taking in consideration the keys and values). That should be simple to achieve.
Another way is to create an array from the Map and then stringify it.
fs.writeFileSync('./msgdata.json', JSON.stringify([...myObject]) , 'utf-8');
You can define Map.prototype.toJSON which works, but it isn't standard and is discouraged. It is automatically called by JSON.stringify if it finds it.
Map.prototype.toJSON = function() {return [...this];};
console.log(JSON.stringify(messages));
A better way is to define your own replacer function (and reviver function too if you need to reconstruct map from JSON). Here I also wrap the map representing array in a special object to tell maps and ordinary arrays apart in JSON:
function replacer (key, value) {
if (value instanceof Map) {
return {
_type: "map",
map: [...value],
}
} else return value;
}
function reviver (key, value) {
if (value._type == "map") return new Map(value.map);
else return value;
}
let str = JSON.stringify(messages, replacer);
let msg = JSON.parse(str, reviver);
The error you are getting about circular structure means that some objects/arrays there contain references to itself. When you store messages you don't need to store all the information related to the channels etc and instead only store ids. So that means extending replacer for TextChannels, ClientUsers, GuildMembers etc:
if (value instanceof TextChannel) {
return {
_type: "TextChannel",
id: value.id,
};
}
var util = require('util');
fs.writeFileSync('./data.json', util.inspect(obj) , 'utf-8');
Ref. Write objects into file with Node.js

Node JS - pull VAR out of array

I trying to scrape data off a website. What is returned to me is an array that looks like.
var $ = cheerio.load(body)
returns:
{ [Function]
fn:
{ constructor: [Circular],
_originalRoot:
{ type: 'root',
name: 'root',
attribs: {},
children: [Object],
next: null,
prev: null,
parent: null } },
load: [Function],
html: [Function],
xml: [Function],
text: [Function],
parseHTML: [Function],
root: [Function],
contains: [Function],
The child object is what I'm after so...
var obj = $.root()[0]
returns:
{ type: 'root',
name: 'root',
attribs: {},
children:
[ { data: 'var usdata_real = { // All_data node opens\n\n\tATL: { // Country tag opens\n\t\tid: "ATL",\n\t\t\tdata: { // Data node begins\n\t\t\tq1_1980: 169.304888373489,\n\t\t\tq2_1980: 164.585093140904,\n\t\t\tq3_1980: 166.167521028595,\n\t\t\tq4_1980: 163.
....
I want the 'data' in this array.
obj = obj['children'][0]['data']
returns:
var usdata_real = { // All_data node opens
ATL: { // Country tag opens
id: "ATL",
data: { // Data node begins
q1_1980: 169.304888373489,
q2_1980: 164.585093140904,
q3_1980: 166.167521028595,
q4_1980: 163.040844286677,
q1_1981: 163.31019903107,
q2_1981: 162.449966027231,
q3_1981: 158.719442789479,
q4_1981: 162.744884934737,
.....
which is ALMOST perfect. Except it returns to me an array which is no bueno. I want to be able treat this as an object so i can do something like
obj['ATL']
and get they KEY/VALUE pairs.
So my question....
Is it possible to strip out the object in this array ('var usdata_real'). Because that is what I'm after.
Any tips would be greatly appreciated.
Thanks!

Loop through Snowflake array

I'm looking for a way to get data from this Collection.
The data looks like:
'0000000' => GuildMember {
guild:
Guild {
members: [Object],
id: '000000',
name: 'Zombie',
_rawVoiceStates: [Object] },
user:
User {
id: '0000000',
username: 'Orc',
_roles: [ '0000' ],
nickname: 'Orc',
joinedTimestamp: 00000,
lastMessageID: null },
'0000000' => GuildMember {
guild:
Guild {
members: [Object],
id: '000000',
name: 'Zombie',
_rawVoiceStates: [Object] },
user:
User {
id: '0000001',
username: 'Orc1',
_roles: [ '0000' ],
nickname: 'Orc',
joinedTimestamp: 00000,
lastMessageID: null },
_array: null,
_keyArray: null }
My current loop is:
var user;
for(var u in test.members){
user = test.members[u];
console.log("["+u+"] "+user.username);
}
It currently kicks back a TypeError: Cannot read property 'user' of null
I originally thought this the data was an array, but it's not according to the Discord.js docs, but I'm still not sure how to pull the username data from the collection.
Any help would be helpful.
Now i looked into the discord.js API and i think what u got todo is something like this (assuming test is your guild object):
test.members.forEach(function(guildMember, guildMemberId) {
console.log(guildMemberId, guildMember.user.username);
})
If that doesn't work try along the lines of:
var membersArray = test.members.array();
for(var guildMemberId in membersArray) {
console.log(guildMemberId, membersArray[guildMemberId].user.username);
}
TypeError: Cannot read property 'user' of null
means your user variable is null which means test.members[u] is null.
Try logging the test.members first and see if its filled.
user.user.username
is probably wrong. As it looks it should be just user.username

How do you iterate over Mongoose model properties within a middleware function?

I'm attempting to iterate over fields in a mongoose model within a middleware function. The current context this is the model object itself. So I have a function in which the context is a Javascript object like this:
{
lastName: 'Wilkens',
firstName: 'Elepart',
name: 'Elepart Wilkens',
username: 'eK',
bio: '<script>alert(\'this is a hack!!\')',
_id: 53b17dd0e8c5af50c1d73bc6,
language: 'en',
country: 'USA',
online: true
}
I want to iterate over this object (which is represented in the current function with this). Every time I attempt to iterate with loops, it prints out values that look like internal Javascript metadata. Is it possible to iterate over this within a function if this represents an object?
Here's the actual middleware function:
userSchema.pre('save', function (next) {
console.log(this); // This prints precisely the same object I have copied above
var fields = Object.keys(this);
for(var i = 0; i < fields.length; i++) {
console.log(this[fields[i]]);
}
for(var key in this) {
if(this.hasOwnProperty(key)) {
console.log(this[key]);
}
}
});
And the output is:
{
strictMode: true,
selected: undefined,
shardval: undefined,
saveError: undefined,
validationError: undefined,
adhocPaths: undefined,
removing: undefined,
inserting: undefined,
version: undefined,
getters: {},
_id: undefined,
populate: undefined,
populated: undefined,
wasPopulated: false,
scope: undefined,
activePaths:
{ paths:
{ username: 'modify',
firstName: 'modify',
name: 'modify',
online: 'default',
country: 'default',
language: 'default',
_id: 'default',
bio: 'modify',
lastName: 'modify' },
states: { default: [Object], init: {}, modify: [Object], require: {} },
stateNames: [ 'require', 'modify', 'init', 'default' ],
map: [Function] },
ownerDocument: undefined,
fullPath: undefined }
true
undefined
0
{ online: true,
country: 'USA',
language: 'en',
_id: 53b1825a00ed9af7c12eedf9,
bio: '<script>alert(\'this is a hack!!\')',
username: 'yK',
name: 'Yershay Wilkens',
firstName: 'Yershay',
lastName: 'Wilkens' }
{ save:
[ { [Function] isAsync: false },
{ [Function: checkForExistingErrors] isAsync: false },
{ [Function: validation] isAsync: false },
{ [Function] isAsync: false },
{ [Function] isAsync: false } ],
validate: [ { [Function] isAsync: false } ] }
{ save: [], validate: [] }
{ [Function] numAsyncPres: 0 }
{ [Function] numAsyncPres: 0 }
{}
You're using integer indexes instead of the string references from the fields array. It should be:
var fields = Object.keys(this);
for(var i = 0; i < fields.length; i++) {
console.log(this[fields[i]]);
}
(e.g., you were doing this[1], this[2], instead of this[fields[1]])
#JohnnyHK's comment worked for me:
const user = new User();
const schemaKeys = Object.keys(user.toObject());
I found a slightly different way to accomplish what I wanted which was to iterate over the model properties of a Mongoose model within a middleware function. This uses async.js, but you could refactor it to use a generic JS loop or any other control flow library. The key is to get an array of the document's fields, then you can iterate over those and get/set the values using the current context with this. As far as I know, this will not coerce non-string values into strings. I've tested it with strings, numbers, booleans and objectIds and they are successfully saved as their original data types.
yourSchema.pre('save', function (next) {
var self = this;
// Get the document's fields
var fields = Object.keys(this._doc);
// Do whatever you want for each field
async.each(fields, function(field, cb) {
self[field] = validator.escape(self[field]);
cb();
}, function(err){
next();
});
});

Categories

Resources