Skip to content

Commit

Permalink
feat: implement javascriptQueryLanguage
Browse files Browse the repository at this point in the history
  • Loading branch information
josdejong committed Nov 14, 2021
1 parent 030d3ca commit 6568afe
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const JSONEditor = _JSONEditor
// plugins
export { createAjvValidator } from './plugins/createAjvValidator.js'
export { lodashQueryLanguage } from './plugins/query/lodashQueryLanguage.js'
export { javascriptQueryLanguage } from './plugins/query/javascriptQueryLanguage.js'
export { jmespathQueryLanguage } from './plugins/query/jmespathQueryLanguage.js'

// utils
Expand Down
110 changes: 110 additions & 0 deletions src/lib/plugins/query/javascriptQueryLanguage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
const description = `
<p>
Enter a JavaScript function to filter, sort, or transform the data.
</p>
`

/** @type {QueryLanguage} */
export const javascriptQueryLanguage = {
id: 'javascript',
name: 'JavaScript',
description,
createQuery,
executeQuery
}

/**
* Turn a path like
*
* ['location', 'latitude']
*
* into a JavaScript selector
*
* '?.["location"]?.["latitude"]'
*
* @param {Path} field
* @returns {string}
*/
function createPropertySelector(field) {
return field.map((f) => `?.[${JSON.stringify(f)}]`).join('')
}

/**
* @param {JSON} json
* @param {QueryLanguageOptions} queryOptions
* @returns {string}
*/
function createQuery(json, queryOptions) {
const { filter, sort, projection } = queryOptions
const queryParts = []

if (filter) {
// Note that the comparisons embrace type coercion,
// so a filter value like '5' (text) will match numbers like 5 too.
const getActualValue = 'item => item' + createPropertySelector(filter.field)

queryParts.push(
` data = data.filter(${getActualValue} ${filter.relation} '${filter.value}')\n`
)
}

if (sort) {
if (sort.direction === 'desc') {
queryParts.push(
` data = data.slice().sort((a, b) => {\n` +
` // sort descending\n` +
` const valueA = a${createPropertySelector(sort.field)}\n` +
` const valueB = b${createPropertySelector(sort.field)}\n` +
` return valueA > valueB ? -1 : valueA < valueB ? 1 : 0\n` +
` })\n`
)
} else {
// sort direction 'asc'
queryParts.push(
` data = data.slice().sort((a, b) => {\n` +
` // sort ascending\n` +
` const valueA = a${createPropertySelector(sort.field)}\n` +
` const valueB = b${createPropertySelector(sort.field)}\n` +
` return valueA > valueB ? 1 : valueA < valueB ? -1 : 0\n` +
` })\n`
)
}
}

if (projection) {
// It is possible to make a util function "pickFlat"
// and use that when building the query to make it more readable.
if (projection.fields.length > 1) {
const fields = projection.fields.map((field) => {
const name = field[field.length - 1] || 'item' // 'item' in case of having selected the whole item
const item = 'item' + createPropertySelector(field)
return ` ${JSON.stringify(name)}: ${item}`
})

queryParts.push(` data = data.map(item => ({\n${fields.join(',\n')}})\n )\n`)
} else {
const field = projection.fields[0]
const item = 'item' + createPropertySelector(field)

queryParts.push(` data = data.map(item => ${item})\n`)
}
}

queryParts.push(' return data\n')

return `function query (data) {\n${queryParts.join('')}}`
}

/**
* @param {JSON} json
* @param {string} query
* @returns {JSON}
*/
function executeQuery(json, query) {
// FIXME: replace unsafe new Function with a JS based query language
// As long as we don't persist or fetch queries, there is no security risk.
// TODO: only import the most relevant subset of lodash instead of the full library?
// eslint-disable-next-line no-new-func
const queryFn = new Function(`'use strict'; return (${query})`)()
return queryFn(json)
}
3 changes: 3 additions & 0 deletions src/lib/plugins/query/jmespathQueryLanguage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import assert from 'assert'
import { parsePath, parseString } from './jmespathQueryLanguage.js'

describe('jmespathQueryLanguage', () => {
// TODO: write tests for createQuery
// TODO: write tests for executeQuery

describe('jsonPath', () => {
it('should parse a json path', () => {
assert.deepStrictEqual(parsePath(''), [])
Expand Down
6 changes: 3 additions & 3 deletions src/lib/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,15 @@
/**
* @typedef {Object} QueryLanguageOptions
* @property {{
* field: string,
* field: Path,
* relation: '==' | '!=' | '<' | '<=' | '>' | '>=',
* value: string
* }} filter
* @property {{
* field: string,
* field: Path,
* direction: 'asc' | 'desc'
* }} sort
* @property {{
* fields: string[]
* fields: Path[]
* }} projection
*/
11 changes: 8 additions & 3 deletions src/routes/development.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
</script>

<script lang="ts">
import { createAjvValidator, JSONEditor, jmespathQueryLanguage, lodashQueryLanguage } from '$lib'
import {
createAjvValidator,
JSONEditor,
jmespathQueryLanguage,
lodashQueryLanguage,
javascriptQueryLanguage
} from '$lib'
import { useLocalStorage } from '$lib/utils/localStorageUtils.js'
import { range } from 'lodash-es'
import Select from 'svelte-select'
let content = {
text: undefined,
Expand Down Expand Up @@ -56,7 +61,7 @@
}
const validator = createAjvValidator(schema)
const queryLanguages = [lodashQueryLanguage, jmespathQueryLanguage]
const queryLanguages = [javascriptQueryLanguage, lodashQueryLanguage, jmespathQueryLanguage]
let queryLanguageId = queryLanguages[0].value
let text = undefined
Expand Down

0 comments on commit 6568afe

Please sign in to comment.