karate.filterKeys() API for complex JSON object - dsl

This question came to my mind after asking another question here
Lets say my response is a complex array of JSON object and currently I test like this for complex object.
* def response = [{id: 1}, {id: 2}, {id: 3}.....]
* def schema = { id: "#number" }
* match response == '#[] schema'
I want to replace above match statement with the use of filerKeys() API possibly like as follows
* match response == karate.filterKeys([]schema, response)
Basically first parameter of karate.filterKeys() API should dynamically accept every JSON object from response array and filter against the second parameter response for a successful match.

I think you are trying to dynamically alter your schema for each JSON value in a JSON array.
you can create an equivalent JSON array schema and do a match ==
* def response = [{id: 1},{id: 2}, {id: 3}]
* def schema = { id: '#number' }
* def fun = function(x){ return karate.filterKeys(schema,x) }
* match response == karate.map(response, fun)

Related

How to splice an object from array of objects?

The thing is i am doing a array of favorites for each user object.
I did the insert request to the array to add some favorite recipe.
the problem is when i want to remove favorite from the array
its always remove the last object and not the exact object i want.
const recipe = await getRecipes(req.params.id); //gives the recipe object
let user = await User.findById(req.user._id); // gives the user object
console.log(recipe);
user.recipes.splice(user.recipes.indexOf(recipe), 1);
await user.save();
res.send(user);
The problem is that your call to indexOf passing the recipe object is not finding the element in the array so it returns -1. See how this code works:
let x = [{id: 1}, {id: 2}, {id: 3}]
let obj = {id: 2}
let i = x.indexOf(obj)
// i is -1 since obj isn't in the array.
// Another object that looks like obj is there,
// but they aren't the same exact object
console.log("i=",i)
// This will remove the last since splicing with -1 does that
x.splice( x.indexOf("d"), 1)
console.log(x)
// when the array has objects in it you can use `findIndex`
let y = [{id: 1}, {id: 2}, {id: 3}]
let j = y.findIndex(e => e.id === obj.id)
console.log("j=",j)
y.splice( j, 1 )
console.log(y)
So what you want to do is find a reliable way to find the index of the recipe in the array. See the 2nd example for how you can find the index within the object. Array.findIndex lets you compare objects in a way that's specific to the object structure.
If you want to remove the object form the array, you can use filter method like this:
user.recipes = user.recipes.filter(r => {
return r !== recipe;
})

JSON.parse fails on ObjectId

I am trying to convert a string for use in mongodb but fails.
let pipeline = JSON.parse('[{"$match": {"_id": ObjectId("5b5637acbd3e9c2068ef80c3")}]');
// results in "SyntaxError: Unexpected token O in JSON at position 20"s
let pipeline = JSON.parse('[{"$match": {"_id": "5b5637acbd3e9c2068ef80c3"}]');
let response = await db.collect('<collection_name>').aggregate(pipeline).toArray();
// returns [] parse works but mongodb doesn't return any rows!
// This works but its not the solution I am looking for.
let pipeline = [{"$match": {"_id": ObjectId("5b5637acbd3e9c2068ef80c3")}];
let response = await db.collect('<collection_name>').aggregate(pipeline).toArray();
I tried using the BSON type but had no luck.
My current work around is to remove the ObjectId() from the string and use a Reviver function with JSON.parse
const ObjectId = require('mongodb').ObjectID;
let convertObjectId = function (key,value){
if (typeof value === 'string' && value.match(/^[0-9a-fA-F]{24}$/)){
return ObjectId(value);
} else {
return value;
};
}
let pipeline = JSON.parse('[{"$match": {"_id": "5b5637acbd3e9c2068ef80c3"}]',convertObjectId);
let response = await db.collect('<collection_name>').aggregate(pipeline).toArray();
// returns one record.
Unfortunately, [{"$match": {"_id": ObjectId("5b5637acbd3e9c2068ef80c3")}] is not valid JSON.
The value of a property in JSON can only be an object (ex.: {}), an array (ex.: []), a string (ex.: "abc"), a number (ex.: 1), a boolean (ex.: true), or null. See an example of these values here: https://en.wikipedia.org/wiki/JSON#Example.
What you could do is add ObjectId() manually after parsing the JSON. This would mean that the value of _id would be a string first, which is valid JSON.
Then, you can loop through your parsed JSON to add ObjectId (see reference here: https://mongodb.github.io/node-mongodb-native/api-bson-generated/objectid.html):
const ObjectId = require('mongodb').ObjectID;
const pipeline = JSON.parse('[{"$match": {"_id": "5b5637acbd3e9c2068ef80c3"}]');
const pipelineWithObjectId = pipeline.map(query => ({
$match: {
...query.$match,
_id: ObjectId(query.$match._id)
}
});
const response = await db.collect('<collection_name>').aggregate(pipelineWithObjectId).toArray();
This should work with the example you provided but there are multiple caveats:
Parsing a query like that could be a vulnerability if the string contains user input that has not been sanitized: https://blog.websecurify.com/2014/08/hacking-nodejs-and-mongodb.html.
This particular code snippet would only work for queries with $match, which means that this code is not very scalable.
This code is not elegant.
All these reasons, for what they are worth, make me think that you would be better off using an object rather than a string for your queries.

Cannot use "==" and "contains" in same line of scenario using conditional logic in Karate

This is a follow up of a question noted here
Lets says our implemented server v1 and v2 response looks as follows
v1Response = { id: "1", name: "awesome" }
v2Response = { id: "2", name: "awesome", value: "karate" }
Similarly we define the client schema for v1 and v2 like as follows
v1Schema = { id: "#string", name: "#string }
v2Schema = { id: "#string", name: "#string, value: "#string" }
We implement schema validation in our generic scenario as follows. We can easily set the "response" with either v1Response/v2Response AND "schema" with either v1Schema/v2Schema depending on our the environment.
* match response == schema
Above generic script works perfectly fine as long as we are testing v1 server against v1 client / v2 server against v2 client. However we cannot re-use the same scenario when we want to test backward compatibility for example
v2 server against v1 client. In this case
* match response (actually v2Response) == schema (actually v1Schema) <--- will fail
So in order to make it work and do backward compatibility testing, I also wanted to use karate "contains" feature like
* match response (actually v2Response) contains schema (actually v1Schema) <--- will pass
However in the quest to keep my scenarios generic it is currently not possible to do either
Use both ==/contains in the same line of script like as follows
serverVersion == clientVersion ? (match response == schema) : (match response contains schema)
OR
Using some flag as follows
match response SOMEFLAG schema
whereas SOMEFLAG can be set to either "==" or "contains" in karate-config.js depending on environment we are testing.
EDIT
From the above example, all I want is to test following cases that should pass
* match v1Response == v1Schema
* match v2Response == v2Schema
* match v2Response contains v1Schema
using a generic line as following
* match response == schema <--- can it possibly be solved using above suggested solutions ?
For some reason you feel that hacking the match clause is the only way to solve this problem. Please keep an open mind and, here you go:
* def schemas =
"""
{
v1: { id: "#string", name: "#string" },
v2: { id: "#string", name: "#string", value: "#string" }
}
"""
* def env = 'v1'
* def response = { id: "1", name: "awesome" }
* match response == schemas[env]
* def env = 'v2'
* def response = { id: "2", name: "awesome", value: "karate" }
* match response == schemas[env]
* def response = { id: "1", name: "awesome" }
* match response == karate.filterKeys(schemas[env], response)
The last line is as generic as you can get.

How to filter results by multiple query parameters if I don't know beforehand how many query strings I may receive from client side?

I want to send in response some data according to searching by query parameters (using .find function of mongoose) from the client side. What do I need to do is a search according to the parameters received?
What I mean is :
I may receive
localhost:5000/admin/customers?customer_id=1&customer_email=abc#gmail.com
I could have used this code to send results according to this query :
Customer.find({
customer_id = req.query.customer_id,
customer_email = req.query.customer_email,
}, (err,docs)=> {
res.json(docs);
})
or
just
localhost:5000/admin/customers?customer_id=1
I could have used this code to send results according to this query :
Customer.find({
customer_id = req.query.customer_id
}, (err,docs)=> {
res.json(docs);
})
or
may be
localhost:5000/admin/customers?no_of_items_purchased=15
I could have used this code to send results according to this query :
Customer.find({
no_of_items_purchased = req.query.no_of_items_purchased
}, (err,docs)=> {
res.json(docs);
})
But what I want is to use .find function on anything received from query params. Like a general code to achieve this.
PS: Also please help with : "How to filter req.query so it only contains fields that are defined in your schema ?"
You can create a query variable to keep the field that you want to filter.
Suppose that your Customer model structure is:
{
customer_id: ...,
customer_name: ...,
customer_email: ...,
no_of_items_purchased: ...
}
Then your code will be:
let {customer_id, customer_name, customer_email, no_of_items_purchased} = req.query;
let query = {};
if (customer_id != null) query.customer_id = customer_id;
if (customer_name != null) query.customer_name = customer_name;
if (customer_email != null) query.customer_email = customer_email;
if (no_of_items_purchased != null) query.no_of_items_purchased = no_of_items_purchased;
let result = await Customer.find(query);
Just pass request.query as a parameter directly on find method:
Customer.find(request.query)

Replacing an object in an object array in Redux Store using Javascript/Lodash

I have an object array in a reducer that looks like this:
[
{id:1, name:Mark, email:mark#email.com},
{id:2, name:Paul, email:paul#gmail.com},
{id:3,name:sally, email:sally#email.com}
]
Below is my reducer. So far, I can add a new object to the currentPeople reducer via the following:
const INITIAL_STATE = { currentPeople:[]};
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case ADD_PERSON:
return {...state, currentPeople: [ ...state.currentPeople, action.payload]};
}
return state;
}
But here is where I'm stuck. Can I UPDATE a person via the reducer using lodash?
If I sent an action payload that looked like this:
{id:1, name:Eric, email:Eric#email.com}
Would I be able to replace the object with the id of 1 with the new fields?
Yes you can absolutely update an object in an array like you want to. And you don't need to change your data structure if you don't want to. You could add a case like this to your reducer:
case UPDATE_PERSON:
return {
...state,
currentPeople: state.currentPeople.map(person => {
if (person.id === action.payload.id) {
return action.payload;
}
return person;
}),
};
This can be be shortened as well, using implicit returns and a ternary:
case UPDATE_PERSON:
return {
...state,
currentPeople: state.currentPeople.map(person => (person.id === action.payload.id) ? action.payload : person),
};
Mihir's idea about mapping your data to an object with normalizr is certainly a possibility and technically it'd be faster to update the user with the reference instead of doing the loop (after initial mapping was done). But if you want to keep your data structure, this approach will work.
Also, mapping like this is just one of many ways to update the object, and requires browser support for Array.prototype.map(). You could use lodash indexOf() to find the index of the user you want (this is nice because it breaks the loop when it succeeds instead of just continuing as the .map would do), once you have the index you could overwrite the object directly using it's index. Make sure you don't mutate the redux state though, you'll need to be working on a clone if you want to assign like this: clonedArray[foundIndex] = action.payload;.
This is a good candidate for data normalization. You can effectively replace your data with the new one, if you normalize the data before storing it in your state tree.
This example is straight from Normalizr.
[{
id: 1,
title: 'Some Article',
author: {
id: 1,
name: 'Dan'
}
}, {
id: 2,
title: 'Other Article',
author: {
id: 1,
name: 'Dan'
}
}]
Can be normalized this way-
{
result: [1, 2],
entities: {
articles: {
1: {
id: 1,
title: 'Some Article',
author: 1
},
2: {
id: 2,
title: 'Other Article',
author: 1
}
},
users: {
1: {
id: 1,
name: 'Dan'
}
}
}
}
What's the advantage of normalization?
You get to extract the exact part of your state tree that you want.
For instance- You have an array of objects containing information about the articles. If you want to select a particular object from that array, you'll have to iterate through entire array. Worst case is that the desired object is not present in the array. To overcome this, we normalize the data.
To normalize the data, store the unique identifiers of each object in a separate array. Let's call that array as results.
result: [1, 2, 3 ..]
And transform the array of objects into an object with keys as the id(See the second snippet). Call that object as entities.
Ultimately, to access the object with id 1, simply do this- entities.articles["1"].
If you want to replace the old data with new data, you can do this-
entities.articles["1"] = newObj;
Use native splice method of array:
/*Find item index using lodash*/
var index = _.indexOf(currentPeople, _.find(currentPeople, {id: 1}));
/*Replace item at index using splice*/
arr.splice(index, 1, {id:1, name:'Mark', email:'mark#email.com'});

Resources