Skip to content

Commit

Permalink
feat: implement support for the query language JSONPath (#470)
Browse files Browse the repository at this point in the history
  • Loading branch information
josdejong authored Jul 25, 2024
1 parent 4edf7dd commit ee6defb
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 3 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ Configure one or multiple query language that can be used in the Transform modal
- [JSONQuery](https://github.com/josdejong/jsonquery)
- [JMESPath](https://jmespath.org/)
- [JSONPath](https://github.com/JSONPath-Plus/JSONPath)
- JavaScript + [Lodash](https://lodash.com/)
- JavaScript
Expand All @@ -607,13 +608,15 @@ The languages can be loaded as follows:
import {
jsonQueryLanguage,
jmespathQueryLanguage,
jsonpathQueryLanguage,
lodashQueryLanguage,
javascriptQueryLanguage
} from 'svelte-jsoneditor'

const allQueryLanguages = [
jsonQueryLanguage,
jmespathQueryLanguage,
jsonpathQueryLanguage,
lodashQueryLanguage,
javascriptQueryLanguage
]
Expand Down
60 changes: 60 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"immutable-json-patch": "6.0.1",
"jmespath": "^0.16.0",
"json-source-map": "^0.6.1",
"jsonpath-plus": "^9.0.0",
"jsonrepair": "^3.8.0",
"lodash-es": "^4.17.21",
"memoize-one": "^6.0.0",
Expand Down Expand Up @@ -127,6 +128,7 @@
"@testing-library/svelte": "5.2.0",
"@types/cookie": "0.6.0",
"@types/jmespath": "0.15.2",
"@types/jsonpath": "0.2.4",
"@types/lodash-es": "4.17.12",
"@typescript-eslint/eslint-plugin": "7.16.0",
"@typescript-eslint/parser": "7.16.0",
Expand Down
3 changes: 2 additions & 1 deletion src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ export * from './plugins/validator/createAjvValidator.js'

// query plugins
export { jsonQueryLanguage } from './plugins/query/jsonQueryLanguage.js'
export { jmespathQueryLanguage } from './plugins/query/jmespathQueryLanguage.js'
export { jsonpathQueryLanguage } from './plugins/query/jsonpathQueryLanguage.js'
export { lodashQueryLanguage } from './plugins/query/lodashQueryLanguage.js'
export { javascriptQueryLanguage } from './plugins/query/javascriptQueryLanguage.js'
export { jmespathQueryLanguage } from './plugins/query/jmespathQueryLanguage.js'

// content
export {
Expand Down
169 changes: 169 additions & 0 deletions src/lib/plugins/query/jsonpathQueryLanguage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { test, describe } from 'vitest'
import assert from 'assert'
import { jsonpathQueryLanguage } from '$lib/plugins/query/jsonpathQueryLanguage'

const { createQuery, executeQuery } = jsonpathQueryLanguage

const user1 = { _id: '1', user: { name: 'Stuart', age: 6, registered: true } }
const user3 = { _id: '3', user: { name: 'Kevin', age: 8, registered: false } }
const user2 = { _id: '2', user: { name: 'Bob', age: 7, registered: true, extra: true } }

const users = [user1, user3, user2]

describe('jsonpathQueryLanguage', () => {
describe('createQuery and executeQuery', () => {
test('should create a and execute an empty query', () => {
const query = createQuery(users, {})
const result = executeQuery(users, query, JSON)
assert.deepStrictEqual(query, '$')
assert.deepStrictEqual(result, [users])
})

test('should create and execute a filter query for a nested property (one match)', () => {
const query = createQuery(users, {
filter: {
path: ['user', 'name'],
relation: '==',
value: 'Bob'
}
})
assert.deepStrictEqual(query, '$[?(@.user.name == "Bob")]')

const result = executeQuery(users, query, JSON)
assert.deepStrictEqual(result, [user2])
})

test('should create and execute a filter query for a nested property (multiple matches)', () => {
const query = createQuery(users, {
filter: {
path: ['user', 'name'],
relation: '!=',
value: 'Bob'
}
})
assert.deepStrictEqual(query, '$[?(@.user.name != "Bob")]')

const result = executeQuery(users, query, JSON)
assert.deepStrictEqual(result, [user1, user3])
})

test('should create and execute a filter query for a property with special characters in the name', () => {
const data = users.map((item) => ({ "user name'": item.user.name }))

const query = createQuery(data, {
filter: {
path: ["user name'"],
relation: '==',
value: 'Bob'
}
})
assert.deepStrictEqual(query, '$[?(@["user name\'"] == "Bob")]')

const result = executeQuery(data, query, JSON)
assert.deepStrictEqual(result, [{ "user name'": 'Bob' }])
})

test('should create and execute a filter query for the whole array item', () => {
const data = [2, 3, 1]
const query = createQuery(data, {
filter: {
path: [],
relation: '==',
value: '1'
}
})
assert.deepStrictEqual(query, '$[?(@ == 1)]')

const result = executeQuery(data, query, JSON)
assert.deepStrictEqual(result, [1])
})

test('should create and execute a filter with booleans', () => {
const query = createQuery(users, {
filter: {
path: ['user', 'registered'],
relation: '==',
value: 'true'
}
})
assert.deepStrictEqual(query, '$[?(@.user.registered == true)]')

const result = executeQuery(users, query, JSON)
assert.deepStrictEqual(result, [user1, user2])
})

test('should create and execute a filter with null', () => {
const query = createQuery(users, {
filter: {
path: ['user', 'extra'],
relation: '!=',
value: 'null'
}
})
assert.deepStrictEqual(query, '$[?(@.user.extra != null)]')

const result = executeQuery(users, query, JSON)
assert.deepStrictEqual(result, [user2])
})

test('should throw an error when trying to sort (not supported by jsonpath)', () => {
assert.throws(() => {
createQuery(users, {
sort: {
path: ['user', 'age'],
direction: 'asc'
}
})
}, /Sorting is not supported by jsonpath. Please clear the sorting fields/)
})

test('should create and execute a project query for a single property', () => {
const query = createQuery(users, {
projection: {
paths: [['user', 'name']]
}
})

assert.deepStrictEqual(query, '$[*].user.name')

const result = executeQuery(users, query, JSON)
assert.deepStrictEqual(result, ['Stuart', 'Kevin', 'Bob'])
})

test('should throw an error when creating a project query for a multiple properties', () => {
assert.throws(() => {
createQuery(users, {
projection: {
paths: [['user', 'name'], ['_id']]
}
})
}, /Error: Picking multiple fields is not supported by jsonpath. Please select only one field/)
})

test('should create and execute a query with filter and project', () => {
const query = createQuery(users, {
filter: {
path: ['user', 'age'],
relation: '<=',
value: '7'
},
projection: {
paths: [['user', 'name']]
}
})

assert.deepStrictEqual(query, '$[?(@.user.age <= 7)].user.name')

const result = executeQuery(users, query, JSON)
assert.deepStrictEqual(result, ['Stuart', 'Bob'])
})

test('should throw an exception when the query is no valid jsonpath expression', () => {
assert.throws(() => {
const data = {}
const query = '@bla bla bla'
executeQuery(data, query, JSON)
}, /TypeError: Unknown value type bla bla b/)
})
})
})
64 changes: 64 additions & 0 deletions src/lib/plugins/query/jsonpathQueryLanguage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { JSONPath as JSONPathPlus } from 'jsonpath-plus'
import { parseString } from '$lib/utils/stringUtils'
import type { QueryLanguage, QueryLanguageOptions } from '$lib/types'
import type { JSONPath } from 'immutable-json-patch'

const description = `
<p>
Enter a <a href="https://github.com/dchester/jsonpath" target="_blank"
rel="noopener noreferrer"><code>jsonpath</code></a> expression to filter, sort, or transform the data.
</p>`

export const jsonpathQueryLanguage: QueryLanguage = {
id: 'jsonpath',
name: 'jsonpath',
description,
createQuery,
executeQuery
}

function createQuery(_json: unknown, queryOptions: QueryLanguageOptions): string {
const { filter, sort, projection } = queryOptions
let expression = '$'

if (filter && filter.path && filter.relation && filter.value) {
const filterValue = parseString(filter.value)
const filterValueStr = JSON.stringify(filterValue)

expression += `[?(@${pathToString(filter.path)} ${filter.relation} ${filterValueStr})]`
}

if (sort && sort.path && sort.direction) {
throw new Error('Sorting is not supported by jsonpath. Please clear the sorting fields')
}

if (projection && projection.paths) {
if (projection.paths.length > 1) {
throw new Error(
'Picking multiple fields is not supported by jsonpath. Please select only one field'
)
}

if (!expression.endsWith(']')) {
expression += '[*]'
}
expression += `${pathToString(projection.paths[0])}`.replace(/^\.\.\./, '..')
}

return expression
}

function executeQuery(json: unknown, path: string): unknown {
const output = JSONPathPlus({ json: json as JSON, path })
return output !== undefined ? output : null
}

function pathToString(path: JSONPath): JSONPath | string {
const lettersOnlyRegex = /^[A-z]+$/

return path
.map((prop) => {
return lettersOnlyRegex.test(prop) ? `.${prop}` : JSON.stringify([prop])
})
.join('')
}
Loading

0 comments on commit ee6defb

Please sign in to comment.