Skip to content

Commit 7d01778

Browse files
committed
fix: add deep equality check and update query matching
1 parent 3570654 commit 7d01778

2 files changed

Lines changed: 75 additions & 11 deletions

File tree

mockServer/src/store/FileStore.js

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,24 +174,64 @@ class FileStore extends StoreAdapter {
174174
}
175175

176176
/**
177-
* Check if a document matches the query
177+
* Deep equality, mirroring NeDB's areThingsEqual: primitives by ===, Dates by
178+
* timestamp, arrays and plain objects by recursive value comparison. An array
179+
* never equals a non-array.
180+
*/
181+
deepEqual(a, b) {
182+
if (a === b) return true
183+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false
184+
185+
const aIsArray = Array.isArray(a)
186+
const bIsArray = Array.isArray(b)
187+
if (aIsArray !== bIsArray) return false
188+
189+
if (a instanceof Date || b instanceof Date) {
190+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime()
191+
}
192+
193+
const aKeys = Object.keys(a)
194+
const bKeys = Object.keys(b)
195+
if (aKeys.length !== bKeys.length) return false
196+
for (const k of aKeys) {
197+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false
198+
if (!this.deepEqual(a[k], b[k])) return false
199+
}
200+
return true
201+
}
202+
203+
/**
204+
* Check if a document matches the query (NeDB-compatible semantics).
205+
* An object query value is treated as an operator object only when every key
206+
* starts with `$`; otherwise (plain object or array) it is deep-compared, so
207+
* `{ config: { mode: 'x' } }` or `{ tags: ['a'] }` does not silently match all
208+
* records. A RegExp query value is matched like `$regex`.
178209
*/
179210
matchesQuery(doc, query) {
180211
if (!query || Object.keys(query).length === 0) {
181212
return true
182213
}
183214

184215
for (const [key, value] of Object.entries(query)) {
185-
if (typeof value === 'object' && value !== null) {
186-
if (value.$regex && !value.$regex.test(doc[key])) return false
187-
if (value.$ne !== undefined && doc[key] === value.$ne) return false
188-
if (value.$in !== undefined && !value.$in.includes(doc[key])) return false
189-
if (value.$nin !== undefined && value.$nin.includes(doc[key])) return false
190-
if (value.$gt !== undefined && !(doc[key] > value.$gt)) return false
191-
if (value.$gte !== undefined && !(doc[key] >= value.$gte)) return false
192-
if (value.$lt !== undefined && !(doc[key] < value.$lt)) return false
193-
if (value.$lte !== undefined && !(doc[key] <= value.$lte)) return false
194-
216+
if (value !== null && typeof value === 'object' && !(value instanceof RegExp)) {
217+
const keys = Object.keys(value)
218+
const isOperatorObject = keys.length > 0 && keys.every((k) => k.startsWith('$'))
219+
220+
if (isOperatorObject) {
221+
if (value.$regex && !value.$regex.test(doc[key])) return false
222+
if (value.$ne !== undefined && doc[key] === value.$ne) return false
223+
if (value.$in !== undefined && !value.$in.includes(doc[key])) return false
224+
if (value.$nin !== undefined && value.$nin.includes(doc[key])) return false
225+
if (value.$gt !== undefined && !(doc[key] > value.$gt)) return false
226+
if (value.$gte !== undefined && !(doc[key] >= value.$gte)) return false
227+
if (value.$lt !== undefined && !(doc[key] < value.$lt)) return false
228+
if (value.$lte !== undefined && !(doc[key] <= value.$lte)) return false
229+
} else if (!this.deepEqual(doc[key], value)) {
230+
// Plain object or array query value: deep-compare (NeDB areThingsEqual)
231+
return false
232+
}
233+
} else if (value instanceof RegExp) {
234+
if (!value.test(doc[key])) return false
195235
} else if (doc[key] !== value) {
196236
// Simple equality check
197237
return false

mockServer/test/store/adapter.contract.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,28 @@ describe.each(adapterFactories)('%s honours the StoreAdapter contract', (_name,
131131
expect(await store.find({ _id: { $nin: ['p1'] } })).toHaveLength(2)
132132
expect(await store.find({ name: { $regex: /^ba/ } })).toHaveLength(2)
133133
})
134+
135+
test('plain object query value deep-compares instead of matching all', async () => {
136+
await store.insert({ _id: 'o1', config: { mode: 'x', n: 1 } })
137+
await store.insert({ _id: 'o2', config: { mode: 'y', n: 2 } })
138+
await store.insert({ _id: 'o3', config: { mode: 'x', n: 3 } })
139+
140+
const matches = await store.find({ config: { mode: 'x', n: 1 } })
141+
expect(matches).toHaveLength(1)
142+
expect(matches[0]._id).toBe('o1')
143+
144+
// a plain object that matches no record must return none — not everything
145+
expect(await store.find({ config: { mode: 'z' } })).toHaveLength(0)
146+
})
147+
148+
test('array query value deep-compares instead of matching all', async () => {
149+
await store.insert({ _id: 'a1', tags: ['red', 'blue'] })
150+
await store.insert({ _id: 'a2', tags: ['green'] })
151+
152+
const matches = await store.find({ tags: ['red', 'blue'] })
153+
expect(matches).toHaveLength(1)
154+
expect(matches[0]._id).toBe('a1')
155+
156+
expect(await store.find({ tags: ['purple'] })).toHaveLength(0)
157+
})
134158
})

0 commit comments

Comments
 (0)