Skip to content

Commit 95d1764

Browse files
committed
fix: function get now returns null instead of undefined when a property doesn't exist
BREAKING CHANGE: Function `get` now returns `null` instead of `undefined` when a property doesn't exist
1 parent 0b79f93 commit 95d1764

File tree

3 files changed

+29
-15
lines changed

3 files changed

+29
-15
lines changed

reference/functions.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,7 @@ jsonquery(data, 'filter(not(.age == 18))')
797797

798798
## exists
799799

800-
Returns true if the value at the provided path exists, and returns false when it is `undefined`.
800+
Returns `true` if the property at the provided path exists. Returns `true` too when the properties value contains a value `null`, `false` or `0`.
801801

802802
```text
803803
exists(path)
@@ -807,18 +807,22 @@ Examples:
807807

808808
```js
809809
const data = [
810-
{ "name": "Chris", "details": { "age": 16 } },
811-
{ "name": "Emily" },
812-
{ "name": "Joe", "details": { "age": 18 } }
810+
{ "name": "Joe", "details": { "age": 16 } },
811+
{ "name": "Oliver" },
812+
{ "name": "Sarah", "details": { "age": 18 } },
813+
{ "name": "Dave", "details": null },
813814
]
814815

815816
jsonquery(data, 'filter(exists(.details))')
816817
// [
817-
// { "name": "Chris", "details": { "age": 16 } },
818-
// { "name": "Joe", "details": { "age": 18 } }
818+
// { "name": "Joe", "details": { "age": 16 } },
819+
// { "name": "Sarah", "details": { "age": 18 } },
820+
// { "name": "Dave", "details": null }
819821
// ]
820822

823+
jsonquery({ }, ["exists", "value"]) // false
821824
jsonquery({ "value": null }, ["exists", "value"]) // true
825+
jsonquery({ "value": undefined }, ["exists", "value"]) // false
822826
```
823827

824828
## in

src/compile.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, test } from 'vitest'
1+
import { bench, describe, expect, test } from 'vitest'
22
import { compile } from './compile'
33
import { buildFunction } from './functions'
44
import type { JSONQuery, JSONQueryCompileOptions } from './types'
@@ -53,7 +53,7 @@ describe('compile', () => {
5353
})
5454

5555
test('should return undefined in case of a non existing path', () => {
56-
expect(go({}, ['get', 'foo', 'bar'])).toEqual(undefined)
56+
expect(go({}, ['get', 'foo', 'bar'])).toEqual(null)
5757
})
5858

5959
test('should get a path using function get', () => {
@@ -179,15 +179,15 @@ describe('compile', () => {
179179
actualErr = err
180180
}
181181

182-
expect(actualErr?.message).toBe("Cannot read properties of undefined (reading 'reduce')")
182+
expect(actualErr?.message).toBe("Cannot read properties of null (reading 'reduce')")
183183
expect(actualErr?.jsonquery).toEqual([
184184
{ data: scoreData, query },
185185
{
186186
data: scoreData.participants,
187187
query: ['map', ['pipe', ['get', 'scores'], ['sum']]]
188188
},
189189
{ data: { name: 'Emily', age: 19 }, query: ['pipe', ['get', 'scores'], ['sum']] },
190-
{ data: undefined, query: ['sum'] }
190+
{ data: null, query: ['sum'] }
191191
])
192192
})
193193
})
@@ -704,7 +704,7 @@ describe('compile', () => {
704704
expect(go({ a: '' }, ['exists', ['get', 'a']])).toEqual(true)
705705
expect(go({ nested: { a: 2 } }, ['exists', ['get', 'nested', 'a']])).toEqual(true)
706706

707-
expect(go({ a: undefined }, ['exists', ['get', 'a']])).toEqual(false)
707+
expect(go({ a: undefined }, ['exists', ['get', 'a']])).toEqual(true)
708708
expect(go({}, ['exists', ['get', 'a']])).toEqual(false)
709709
expect(go({}, ['exists', ['get', 'nested', 'a']])).toEqual(false)
710710
expect(go({}, ['exists', ['get', 'sort']])).toEqual(false)

src/functions.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
Getter,
77
JSONPath,
88
JSONQuery,
9+
JSONQueryFunction,
910
JSONQueryObject,
1011
JSONQueryProperty
1112
} from './types'
@@ -52,12 +53,12 @@ export const functions: FunctionBuildersMap = {
5253

5354
get: (...path: JSONPath) => {
5455
if (path.length === 0) {
55-
return (data: unknown) => data
56+
return (data: unknown) => data ?? null
5657
}
5758

5859
if (path.length === 1) {
5960
const prop = path[0]
60-
return (data: unknown) => data?.[prop]
61+
return (data: unknown) => data?.[prop] ?? null
6162
}
6263

6364
return (data: unknown) => {
@@ -67,7 +68,7 @@ export const functions: FunctionBuildersMap = {
6768
value = value?.[prop]
6869
}
6970

70-
return value
71+
return value ?? null
7172
}
7273
},
7374

@@ -210,7 +211,16 @@ export const functions: FunctionBuildersMap = {
210211
and: buildFunction((a, b) => a && b),
211212
or: buildFunction((a, b) => a || b),
212213
not: buildFunction((a: unknown) => !a),
213-
exists: buildFunction((a: unknown) => a !== undefined),
214+
exists: (path: JSONQueryFunction) => {
215+
const parentPath = path.slice(1)
216+
const key = parentPath.pop()
217+
const getter = functions.get(...parentPath)
218+
219+
return (data: unknown) => {
220+
const parent = getter(data)
221+
return !!parent && Object.hasOwnProperty.call(parent, key)
222+
}
223+
},
214224

215225
eq: buildFunction((a, b) => a === b),
216226
gt: buildFunction((a, b) => a > b),

0 commit comments

Comments
 (0)