Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle duplication of results when filtering using nested fields #117

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions src/__tests__/cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ const docs = {
one: {
_id: objectID,
foo: 'bar',
tags: ['foo', 'bar']
tags: ['foo', 'bar'],
nested: {
field1: 'one',
field2: 'two'
}
},
two: {
_id: ObjectId(),
foo: 'bar'
foo: 'bar',
nested: {
field1: 'two',
field2: 'one'
}
},
three: {
nested: {
Expand Down Expand Up @@ -214,6 +222,30 @@ describe('createCachingMethods', () => {
expect(collection.find.mock.calls.length).toBe(1)
})

it(`doesn't mix results of pending calls using nested field filters with differing values`, async () => {
const pendingDocs1 = api.findByFields({
'nested.field1': 'one',
'nested.field2': 'two'
})
const pendingDocs2 = api.findByFields({
'nested.field1': 'two',
'nested.field2': 'one'
})

const [foundDocs1, foundDocs2] = await Promise.all([
pendingDocs1,
pendingDocs2
])

expect(foundDocs1[0]).toBe(docs.one)
expect(foundDocs1.length).toBe(1)
expect(foundDocs2[0]).toBe(docs.two)
expect(foundDocs2.length).toBe(1)

expect(collection.find.mock.calls.length).toBe(1)
console.log(collection.find.mock.calls[0])
})

it(`dataloader caches each value individually when finding by a single field`, async () => {
await api.findByFields({ tags: ['foo', 'baz'] }, { ttl: 1 })

Expand Down Expand Up @@ -318,12 +350,14 @@ describe('isValidObjectIdString', () => {
describe('getNestedValue', () => {
it('works', () => {
const obj = {
nested: { foo: 'bar', fooB: '', fooC: null, fooE: { inner: true } }
nested: { foo: 'bar', fooB: '', fooC: null, fooE: { inner: true } },
['top.level']: 'baz'
}
expect(getNestedValue(obj, 'nested.foo')).toEqual('bar')
expect(getNestedValue(obj, 'nested.fooB')).toEqual('')
expect(getNestedValue(obj, 'nested.fooC')).toEqual(null)
expect(getNestedValue(obj, 'nested.fooD')).toBeUndefined()
expect(getNestedValue(obj, 'nested.fooE.inner')).toBe(true)
expect(getNestedValue(obj, 'top.level')).toBe('baz')
})
})
114 changes: 111 additions & 3 deletions src/__tests__/datasource.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ describe('Mongoose', () => {
let userCollection
let alice
let nestedBob
let nestedCharlie
let nestedDan

beforeAll(async () => {
const userSchema = new Schema({ name: 'string' })
Expand All @@ -64,7 +66,28 @@ describe('Mongoose', () => {

nestedBob = await userCollection.findOneAndReplace(
{ name: 'Bob' },
{ name: 'Bob', nested: { _id: objectID, field1: 'value1', field2: '' } },
{
name: 'Bob',
nested: { _id: objectID, field1: 'value1', field2: 'value1' }
},
{ new: true, upsert: true }
)

nestedCharlie = await userCollection.findOneAndReplace(
{ name: 'Charlie' },
{
name: 'Charlie',
nested: { field1: 'value2', field2: 'value2' }
},
{ new: true, upsert: true }
)

nestedDan = await userCollection.findOneAndReplace(
{ name: 'Dan' },
{
name: 'Dan',
nested: { field1: 'value1', field2: 'value2' }
},
{ new: true, upsert: true }
)
})
Expand Down Expand Up @@ -130,9 +153,94 @@ describe('Mongoose', () => {
expect(user.name).toBe('Bob')

const res1 = await users.findByFields({ 'nested.field1': 'value1' })
const res2 = await users.findByFields({ 'nested.field2': 'value1' })

const res2 = await users.findByFields({ 'nested.field2': '' })
expect(res1[0].name).toBe('Bob')
expect(res2[0]).toBeUndefined()
})

test('nested findByFields with single filter and batching', async () => {
const users = new Users(userCollection)
users.initialize()

let pendingDocs = []
for (var i = 1; i <= 3; i++) {
pendingDocs.push(users.findByFields({ 'nested.field1': `value${i}` }))
}

/*
Intent here, with Promise.All, is to force batching to happen in the underlying dataloader library.

This results in the following optimized filter to be passed to MongoDb:

filter: {
'nested.field1': { '$in': [ 'value1', 'value2', 'value3' ] }
}

This in turn correctly matches Bob, Charlie and Dan records.

Bob and Dan match filters passed to the first invocation of findByFields function: { 'nested.field1': [ 'value1' ] }

Charlie matches filters passed to the second invocation of findByFields function: { 'nested.field1': [ 'value2' ] }
*/

const docs = await Promise.all(pendingDocs)

expect(docs[0][0].name).toBe('Bob')
expect(docs[0][1].name).toBe('Dan')
expect(docs[0].length).toBe(2)

expect(docs[1][0].name).toBe('Charlie')
expect(docs[1].length).toBe(1)

expect(docs[2][0]).toBeUndefined()
expect(docs[2].length).toBe(0)

expect(docs.length).toBe(3)
})

test('nested findByFields with multiple filters and batching', async () => {
const users = new Users(userCollection)
users.initialize()

let pendingDocs = []
for (var i = 1; i <= 3; i++) {
pendingDocs.push(
users.findByFields({
'nested.field1': `value${i}`,
'nested.field2': `value${i}`
})
)
}

/*
Intent here, with Promise.All, is to force batching to happen in the underlying dataloader library.
This results in the following optimized filter to be passed to MongoDb:

filter: {
'nested.field1': { '$in': [ 'value1', 'value2', 'value3' ] },
'nested.field2': { '$in': [ 'value1', 'value2', 'value3' ] }
}

This in turn correctly matches Bob, Charlie and Dan records.

However, only Bob and Charlie match original filters passed to findByFields function, so only those should be returned.

{ 'nested.field1': [ 'value1' ], 'nested.field2': [ 'value1' ] },
{ 'nested.field1': [ 'value2' ], 'nested.field2': [ 'value2' ] }

*/

const docs = await Promise.all(pendingDocs)

expect(docs[0][0].name).toBe('Bob')
expect(docs[0].length).toBe(1)

expect(docs[1][0].name).toBe('Charlie')
expect(docs[1].length).toBe(1)

expect(docs[2][0]).toBeUndefined()
expect(docs[2].length).toBe(0)

expect(docs.length).toBe(3)
})
})
9 changes: 8 additions & 1 deletion src/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ export function prepFields(fields) {

// getNestedValue({ nested: { foo: 'bar' } }, 'nested.foo')
// => 'bar'
// getNestedValue({ 'top.level': value }, 'top.level')
// => 'value'
export function getNestedValue(object, string) {
if (string in object) {
return object[string]
}

string = string.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties
string = string.replace(/^\./, '') // strip a leading dot
var a = string.split('.')
Expand Down Expand Up @@ -76,7 +82,8 @@ const orderDocs = (fieldsArray, docs) =>
? fieldValue.map(val => idToString(val))
: [idToString(fieldValue)]

const docValue = doc[fieldName]
const docValue = getNestedValue(doc, fieldName)

const docValuesArr = Array.isArray(docValue)
? docValue.map(val => idToString(val))
: [idToString(docValue)]
Expand Down