In my server I use both MongoDB and Neo4j, and when I update a mongo product record I also update Neo4j product node and relationship to a category node. I use a mongo transaction so if anything fails consistence is maintained across the two dbs.
In my mongo update method I can either update rating or the whole record,so I set up tests to do both.
I pass to the Neo4j update method the js object from returning the updated mongo record, for the first test I modify the category parameter, so in Neo4j it first deletes the relationship to the old category, then it updates the product node and creates a relationship to the new category.
The first test passes ( update category), but when the second test runs (updating rating) it fails as the Neo4j method throws Cannot read properties of undefined (reading 'get') error.
I actually modified the cypher as my first draft ( the commented out one) wasn't updating relationship to the category node and both tests passed.
Thinking that the problem might be that in the second test there is no difference in category I duplicate the first test and indeed the second time it doesn't pass either.
Can you please spot what am I doing wrong?
Many thanks.
Neo4J
exports.updateProductById = async (product) => {
console.log('Neo4j updateProductById product is : ', product);
const driver = neo4j.getDriver();
const session = driver.session();
const json = JSON.stringify(product);
const res = await session.executeWrite(tx => tx.run(
// `
// with apoc.convert.fromJsonMap($json) as json
// match (p:Product {id: json.id}) set p = json
// with p, json
// match (s:Shop)-[r:SELLS]->(p)-[r2:IN_CATEGORY]->(c:Category)
// set r.productId = json.id, c.name = json.category
// RETURN p as product,s as shop, r as relSells, r2 as relCategory
// `
`
with apoc.convert.fromJsonMap($json) as json
match (p:Product {id: json.id})
match (s:Shop)-[r:SELLS]->(p)-[rOld:IN_CATEGORY]->(c:Category)
where c.name = p.category
set p = json
delete rOld
merge (p)-[rNew:IN_CATEGORY]->(cat: category {name : json.category})
set rNew.productId = json.id
RETURN p as product, s as shop, r as relSells, rNew as relCategory, rOld as relOld, c as catOld, cat as category
`
,{json: json }
)).catch((error) => {
console.log('Neo4j updateProductById error: ', error);
});
await session.close();
console.log(`Neo4j updateProductById modified ${res.records.length} products`);
const updatedProduct = res.records[0].get('product');
const shop = res.records[0].get('shop');
const relSells = res.records[0].get('relSells');
const relCategory = res.records[0].get('relCategory');
const category = res.records[0].get('category');
const catOld = res.records[0].get('catOld');
const relOld = res.records[0].get('relOld');
console.log('Neo4j updateProductById modified product is: ', updatedProduct);
console.log('Neo4j updateProductById shop is: ', shop);
console.log('Neo4j updateProductById modified relSells is: ', relSells);
console.log('Neo4j updateProductById modified relCategory is: ', relCategory);
console.log('Neo4j updateProductById modified category is: ', category);
console.log('Neo4j updateProductById old category is: ', catOld);
console.log('Neo4j updateProductById old relCategory is: ', relOld);
return updatedProduct;
}
Whole record update console logs
Mongoose updateProductById: {
_id: new ObjectId("63417b1e6073ddba42d0ddf3"),
createdOnDate: 1638894572905,
name: 'someNewName',
brand: 'someCategory',
price: 12,
description: 'description',
category: 'Bikes',
city: 'Bologna',
region: 'Emilia-Romagna',
country: 'Italy',
vendor: 'fixit',
vendorId: 'test2',
barcode: 'some',
imageUrl: 'https://firebasestorage.googleapis.com/v0/b/fix-it-b4b00.appspot.com/o/Products%2F61af8bec02edbe24ce034963?alt=media&token=a891dc05-407e-43d2-ab2b-0f49226249a9',
fullImages: [],
thumbNails: [],
minimumStock: 10,
availableQuantity: 10,
soldQuantity: 0,
isPromotion: false,
totalRating: 0,
ratings: 0,
createdAt: 2022-10-08T13:29:02.880Z,
updatedAt: 2022-10-08T13:29:02.961Z,
__v: 0,
averageRating: 0,
id: '63417b1e6073ddba42d0ddf3'
}
Neo4j updateProductById product is : {
_id: new ObjectId("63417b1e6073ddba42d0ddf3"),
createdOnDate: 1638894572905,
name: 'someNewName',
brand: 'someCategory',
price: 12,
description: 'description',
category: 'Bikes',
city: 'Bologna',
region: 'Emilia-Romagna',
country: 'Italy',
vendor: 'fixit',
vendorId: 'test2',
barcode: 'some',
imageUrl: 'https://firebasestorage.googleapis.com/v0/b/fix-it-b4b00.appspot.com/o/Products%2F61af8bec02edbe24ce034963?alt=media&token=a891dc05-407e-43d2-ab2b-0f49226249a9',
fullImages: [],
thumbNails: [],
minimumStock: 10,
availableQuantity: 10,
soldQuantity: 0,
isPromotion: false,
totalRating: 0,
ratings: 0,
createdAt: 2022-10-08T13:29:02.880Z,
updatedAt: 2022-10-08T13:29:02.961Z,
__v: 0,
averageRating: 0,
id: '63417b1e6073ddba42d0ddf3'
}
Neo4j updateProductById modified 1 products
Neo4j updateProductById modified product is: Node {
identity: Integer { low: 2, high: 0 },
labels: [ 'Product' ],
properties: {
country: 'Italy',
isPromotion: false,
city: 'Bologna',
vendorId: 'test2',
description: 'description',
fullImages: [],
soldQuantity: Integer { low: 0, high: 0 },
createdAt: '2022-10-08T13:29:02.880Z',
price: Integer { low: 12, high: 0 },
ratings: Integer { low: 0, high: 0 },
vendor: 'fixit',
averageRating: Integer { low: 0, high: 0 },
__v: Integer { low: 0, high: 0 },
imageUrl: 'https://firebasestorage.googleapis.com/v0/b/fix-it-b4b00.appspot.com/o/Products%2F61af8bec02edbe24ce034963?alt=media&token=a891dc05-407e-43d2-ab2b-0f49226249a9',
minimumStock: Integer { low: 10, high: 0 },
id: '63417b1e6073ddba42d0ddf3',
brand: 'someCategory',
barcode: 'some',
updatedAt: '2022-10-08T13:29:02.961Z',
thumbNails: [],
availableQuantity: Integer { low: 10, high: 0 },
totalRating: Integer { low: 0, high: 0 },
createdOnDate: Integer { low: -1782934167, high: 381 },
name: 'someNewName',
_id: '63417b1e6073ddba42d0ddf3',
category: 'Bikes',
region: 'Emilia-Romagna'
},
elementId: '2'
}
Neo4j updateProductById shop is: Node {
identity: Integer { low: 1, high: 0 },
labels: [ 'Shop' ],
properties: { id: 'test2' },
elementId: '1'
}
Neo4j updateProductById modified relSells is: Relationship {
identity: Integer { low: 5, high: 0 },
start: Integer { low: 1, high: 0 },
end: Integer { low: 2, high: 0 },
type: 'SELLS',
properties: { productId: '63417b1e6073ddba42d0ddf3' },
elementId: '5',
startNodeElementId: '1',
endNodeElementId: '2'
}
Neo4j updateProductById modified relCategory is: Relationship {
identity: Integer { low: 1, high: 0 },
start: Integer { low: 2, high: 0 },
end: Integer { low: 8, high: 0 },
type: 'IN_CATEGORY',
properties: { productId: '63417b1e6073ddba42d0ddf3' },
elementId: '1',
startNodeElementId: '2',
endNodeElementId: '8'
}
Neo4j updateProductById modified category is: Node {
identity: Integer { low: 8, high: 0 },
labels: [ 'category' ],
properties: { name: 'Bikes' },
elementId: '8'
}
Neo4j updateProductById old category is: Node {
identity: Integer { low: 4, high: 0 },
labels: [ 'Category' ],
properties: { name: 'Safety and locks' },
elementId: '4'
}
Neo4j updateProductById old relCategory is: Relationship {
identity: Integer { low: 0, high: 0 },
start: Integer { low: -1, high: -1 },
end: Integer { low: -1, high: -1 },
type: '',
properties: {},
elementId: '0',
startNodeElementId: '-1',
endNodeElementId: '-1'
}
Mongoose updateProductById Neo4j updated product
transacion wasn't aborted
committing session
ending session
Update rating console logs
Mongoose updateProductById rating: {
_id: new ObjectId("63417b1e6073ddba42d0ddf3"),
createdOnDate: 1638894572905,
name: 'someNewName',
brand: 'someCategory',
price: 12,
description: 'description',
category: 'Bikes',
city: 'Bologna',
region: 'Emilia-Romagna',
country: 'Italy',
vendor: 'fixit',
vendorId: 'test2',
barcode: 'some',
imageUrl: 'https://firebasestorage.googleapis.com/v0/b/fix-it-b4b00.appspot.com/o/Products%2F61af8bec02edbe24ce034963?alt=media&token=a891dc05-407e-43d2-ab2b-0f49226249a9',
fullImages: [],
thumbNails: [],
minimumStock: 10,
availableQuantity: 10,
soldQuantity: 0,
isPromotion: false,
totalRating: 5,
ratings: 1,
createdAt: 2022-10-08T13:29:02.880Z,
updatedAt: 2022-10-08T13:29:03.096Z,
__v: 0,
averageRating: 5,
id: '63417b1e6073ddba42d0ddf3'
}
Neo4j updateProductById product is : {
_id: new ObjectId("63417b1e6073ddba42d0ddf3"),
createdOnDate: 1638894572905,
name: 'someNewName',
brand: 'someCategory',
price: 12,
description: 'description',
category: 'Bikes',
city: 'Bologna',
region: 'Emilia-Romagna',
country: 'Italy',
vendor: 'fixit',
vendorId: 'test2',
barcode: 'some',
imageUrl: 'https://firebasestorage.googleapis.com/v0/b/fix-it-b4b00.appspot.com/o/Products%2F61af8bec02edbe24ce034963?alt=media&token=a891dc05-407e-43d2-ab2b-0f49226249a9',
fullImages: [],
thumbNails: [],
minimumStock: 10,
availableQuantity: 10,
soldQuantity: 0,
isPromotion: false,
totalRating: 5,
ratings: 1,
createdAt: 2022-10-08T13:29:02.880Z,
updatedAt: 2022-10-08T13:29:03.096Z,
__v: 0,
averageRating: 5,
id: '63417b1e6073ddba42d0ddf3'
}
Neo4j updateProductById modified 0 products
Mongoose updateProductById Neo4j error: TypeError: Cannot read properties of undefined (reading 'get')
at exports.updateProductById (/Users/vincenzocalia/server-node/api/src/neo4j/product_neo4j.js:132:43)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async exports.updateProductById (/Users/vincenzocalia/server-node/api/src/controllers/product.controller.js:840:9)
Transaction error is: TypeError: Cannot read properties of undefined (reading 'get')
at exports.updateProductById (/Users/vincenzocalia/server-node/api/src/neo4j/product_neo4j.js:132:43)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async exports.updateProductById (/Users/vincenzocalia/server-node/api/src/controllers/product.controller.js:840:9)
transacion was aborted
ending session
Found the problem being I use merge for the entire pattern where (cat: Category) might need to be created, while (p) existed already
merge (p)-[rNew:IN_CATEGORY]->(cat: Category {name : json.category}) as mentioned in the docs
When using MERGE on full patterns, the behavior is that either the whole pattern matches, or the whole pattern is created. MERGE will not partially use existing patterns — it is all or nothing. If partial matches are needed, this can be accomplished by splitting a pattern up into multiple MERGE clauses.
So after splitting it as follows it worked as intended.
`
with apoc.convert.fromJsonMap($json) as json
match (p:Product {id: json.id})
match (s:Shop)-[r:SELLS]->(p)-[rOld:IN_CATEGORY]->(c:Category)
where c.name = p.category
set p = json
delete rOld
merge (cat: Category {name : json.category})
merge (p)-[rNew:IN_CATEGORY]->(cat)
set rNew.productId = json.id
RETURN p as product, s as shop, r as relSells, rNew as relCategory, rOld as relOld, c as catOld, cat as category
`
Hope this will help other starting with Neo4j.
Cheers
I was using jest to write a test for a function and I keep getting hit with an exceeded timeout issue. I tried to solve this by adding a bigger number to the second parameter in my test function and also by changing my jest config settings in my package.json file but both did not work. Any help would be appreciated.
Test File
const favoriteBlog = require('../utils/list_helper').favoriteBlog
describe(("fav blog"), () => {
const listWithOneBlog = [
{
_id: "5a422a851b54a676234d17f7",
title: "React patterns",
author: "Michael Chan",
url: "https://reactpatterns.com/",
likes: 7,
__v: 0
},
{
_id: "5a422aa71b54a676234d17f8",
title: "Go To Statement Considered Harmful",
author: "Edsger W. Dijkstra",
url: "http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html",
likes: 5,
__v: 0
},
{
_id: "5a422b3a1b54a676234d17f9",
title: "Canonical string reduction",
author: "Edsger W. Dijkstra",
url: "http://www.cs.utexas.edu/~EWD/transcriptions/EWD08xx/EWD808.html",
likes: 12,
__v: 0
},
{
_id: "5a422b891b54a676234d17fa",
title: "First class tests",
author: "Robert C. Martin",
url: "http://blog.cleancoder.com/uncle-bob/2017/05/05/TestDefinitions.htmll",
likes: 10,
__v: 0
},
{
_id: "5a422ba71b54a676234d17fb",
title: "TDD harms architecture",
author: "Robert C. Martin",
url: "http://blog.cleancoder.com/uncle-bob/2017/03/03/TDD-Harms-Architecture.html",
likes: 0,
__v: 0
},
{
_id: "5a422bc61b54a676234d17fc",
title: "Type wars",
author: "Robert C. Martin",
url: "http://blog.cleancoder.com/uncle-bob/2016/05/01/TypeWars.html",
likes: 2,
__v: 0
}
]
test("find the blog with the most likes", (blog) => {
const expected = {
title: "Canonical string reduction",
author: "Edsger W. Dijkstra",
likes: 12
}
const result = favoriteBlog(listWithOneBlog)
expect(result).toEqual(expected)
})
})
Function File
const favoriteBlog = (blog) => {
console.log("before is",blog.map(a => a.likes) )
const preArray = blog.map(a => a.likes)
console.log("Math max thing is", Math.max(...preArray))
const objArr = Math.max(...preArray)
console.log("objArr is",objArr)
const favBlog = blog.filter((x) => x.likes == objArr);
const favBlogObj = favBlog[0]
delete favBlogObj._id;
delete favBlogObj.__v;
delete favBlogObj.url;
console.log("Fav blog is now",favBlog)
return favBlogObj
}
module.exports = {
favoriteBlog
}
The test is failing because you have a blog parameter on the test callback, which Jest interprets to be a callback function which you must call to end the test. The param is not used by the test, so I think this is a mistake and should be removed. If you intend to use the callback, then just add blog() as the last statement in the test (perhaps renaming it to done which is the convention).
Completion callbacks are used for some API styles, but in most cases you don't need this parameter and can just end the test, or return a Promise if you are expecting something to end later.
test("find the blog with the most likes", (blog) => {
// ^^^^ HERE
const expected = {
title: "Canonical string reduction",
author: "Edsger W. Dijkstra",
likes: 12
}
const result = favoriteBlog(listWithOneBlog)
expect(result).toEqual(expected)
})
Restaurants is a collection and has objects like below:
{
_id: new ObjectId("61723c7378b6d3a5a02d908e"),
name: 'The Blue Hotel',
location: 'Noon city, New York',
phoneNumber: '122-536-7890',
website: 'http://www.bluehotel.com',
priceRange: '$$$',
cuisines: [ 'Mexican', 'Italian' ],
overallRating: 0,
serviceOptions: { dineIn: true, takeOut: true, delivery: true },
reviews: [
{
_id: new ObjectId("61736a0f65b9931b9e428789"),
title: 'asd',
reviewer: 'khoh',
rating: 3,
dateOfReview: '5/12/2002',
review: 'hey'
},
_id: new ObjectId("61736a0f65b9931b9e428790"),
title: 'dom',
reviewer: 'firuu',
rating: 4,
dateOfReview: '25/1/2002',
review: ' bruh'
}
]
}
I am using the below code to find this object based on the review id provided
async get(reviewId) {
const restaurantsCollection = await restaurants();
reviewId = ObjectId(reviewId)
const r = await restaurantsCollection.findOne({reviews: {$elemMatch: {_id: reviewId}}})
return r
This returns the whole object from the restaurant collection, what do I do if I want only the review displayed whose id is provided in get(reviewID)
Output:
{
_id: new ObjectId("61736a0f65b9931b9e428790"),
title: 'dom',
reviewer: 'firuu',
rating: 4,
dateOfReview: '25/1/2002',
review: ' bruh'
}
With a projection, specify the fields to return
The following returns only the review whose id is provided in get(reviewID)
async get(reviewId) {
const restaurantsCollection = await restaurants();
reviewId = ObjectId(reviewId)
const r = await restaurantsCollection.findOne(
{ reviews: { $elemMatch: { _id: reviewId } } },
{ "reviews.$": 1 }
)
return r
}
Test Here
You can also use find instead of fineOne
Query
replace the ObjectId("61736a0f65b9931b9e428789") with reviewId
this will return the reviews that match the _id in an array
if you want to get the first only, in case there is always max 1
you can replace the last project with
{"$project": {"_id": 0, "review": {"$arrayElemAt": ["$reviews", 0]}}}
*not sure if this is what you need
Test code here
aggregate(
[{"$match": {"reviews._id": ObjectId("61736a0f65b9931b9e428789")}}
{"$set":
{"reviews":
{"$filter":
{"input": "$reviews",
"cond":
{"$eq": ["$$this._id", ObjectId("61736a0f65b9931b9e428789")]}}}}},
{"$project": {"_id": 0, "reviews": 1}}])
This might not be the correct answer of your question but you can try something like this.
const r = await restaurantsCollection.findOne({reviews: {$elemMatch: {_id: reviewId}}})?.reviews.find(review => review._id.equals(reviewId))
I have fulltext enabled with MongoDB. Here is my script:
function bestMatch(name){
ChebiEntry.find({$text: {$search: escapeString(name)}},
{score: {$meta: 'textScore'}}, {lean:true},(e,results)=>{
if (results.length > 0){
let oneScores = []
results.forEach((obj)=>{
if(obj.score===1){
oneScores.push(obj);
}
});
console.log(oneScores);
}
})
.sort({score: {$meta: 'textScore'}})
}
//prepareDatabase();
bestMatch('K+');
And here is my output:
[ { _id: 596d55fd2c446456a0cebc1d,
id: 8345,
name: 'K+',
__v: 0,
score: 1 },
{ _id: 596d55ff2c446456a0ceccb2,
id: 8345,
name: 'K(+)',
__v: 0,
score: 1 },
{ _id: 596d56072c446456a0ceee6b,
id: 29103,
name: 'K(+)',
__v: 0,
score: 1 },
{ _id: 596d56072c446456a0cef092,
id: 29104,
name: 'K(-)',
__v: 0,
score: 1 } ]
I have a hit that is verbatim what I searched. However it has the same score as several other hits that are not exactly what I searched. This 'score' system that mongoDB is not very helpful like this.
Is there a way to change this score system to give verbatim matches a higher score? There are also results that are over 1, which are even worse hits for my query.