Implement proof of concept for `createQuery` and `executeQuery`, see #857
This commit is contained in:
parent
4eb55bffde
commit
d2332dc308
|
@ -0,0 +1,99 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>JSONEditor | Custom query language</title>
|
||||
<meta charset="utf-8" />
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
<script src="../dist/jsoneditor.js"></script>
|
||||
<script src="https://unpkg.com/lodash@4.17.15/lodash.min.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
p {
|
||||
max-width: 500px;
|
||||
font-family: sans-serif;
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 11pt;
|
||||
background: #e5e5e5;
|
||||
}
|
||||
|
||||
#jsoneditor {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
This demo shows how to configure a custom query language.
|
||||
Click on the "Transform" button and try it out.
|
||||
</p>
|
||||
<p>
|
||||
The example uses lodash functions <code>filter</code>, <code>sort</code>, and <code>pick</code>.
|
||||
</p>
|
||||
<p class="warning">
|
||||
WARNING: this example uses <code>new Function()</code> which can be dangerous when executed with arbitrary code.
|
||||
</p>
|
||||
<div id="jsoneditor"></div>
|
||||
|
||||
<script>
|
||||
const container = document.getElementById('jsoneditor')
|
||||
const options = {
|
||||
createQuery: function (json, queryOptions) {
|
||||
console.log('createQuery', queryOptions)
|
||||
|
||||
const { filter, sort, projection } = queryOptions
|
||||
let query = 'data'
|
||||
|
||||
if (filter) {
|
||||
// FIXME: parse filter.value either as string or number
|
||||
query = `_.filter(${query}, item => _.get(item, '${filter.field}') ${filter.relation} '${filter.value}')`
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
query = `_.orderBy(${query}, '${sort.field}', '${sort.direction}')`
|
||||
}
|
||||
|
||||
if (projection) {
|
||||
query = `_.map(${query}, item => _.pick(item, ${JSON.stringify(projection.fields)}))`
|
||||
}
|
||||
|
||||
return query
|
||||
},
|
||||
executeQuery: function (json, query) {
|
||||
console.log('executeQuery', query)
|
||||
|
||||
// WARNING: Using new Function() with arbitrary input can be dangerous! Be careful.
|
||||
const execute = new Function('data', 'return ' + query)
|
||||
|
||||
return execute(json)
|
||||
}
|
||||
}
|
||||
const json = []
|
||||
for (let i = 0; i < 100; i++) {
|
||||
var longitude = 4 + i / 100
|
||||
var latitude = 51 + i / 100
|
||||
|
||||
json.push({
|
||||
name: 'Item ' + i,
|
||||
id: String(i),
|
||||
index: i,
|
||||
time: new Date().toISOString(),
|
||||
location: {
|
||||
latitude: longitude,
|
||||
longitude: latitude,
|
||||
coordinates: [longitude, latitude]
|
||||
},
|
||||
random: Math.random()
|
||||
})
|
||||
}
|
||||
const editor = new JSONEditor(container, options, json)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -183,7 +183,7 @@ JSONEditor.VALID_OPTIONS = [
|
|||
'timestampTag', 'timestampFormat',
|
||||
'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation',
|
||||
'sortObjectKeys', 'navigationBar', 'statusBar', 'mainMenuBar', 'languages', 'language', 'enableSort', 'enableTransform',
|
||||
'maxVisibleChilds', 'onValidationError'
|
||||
'maxVisibleChilds', 'onValidationError', 'createQuery', 'executeQuery'
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
'use strict'
|
||||
|
||||
import jmespath from 'jmespath'
|
||||
import naturalSort from 'javascript-natural-sort'
|
||||
import { createAbsoluteAnchor } from './createAbsoluteAnchor'
|
||||
import { ContextMenu } from './ContextMenu'
|
||||
|
@ -3309,7 +3308,7 @@ export class Node {
|
|||
|
||||
// apply the JMESPath query
|
||||
const oldValue = this.getValue()
|
||||
const newValue = jmespath.search(oldValue, query)
|
||||
const newValue = this.editor.options.executeQuery(oldValue, query)
|
||||
this.setValue(newValue)
|
||||
|
||||
const newInternalValue = this.getInternalValue()
|
||||
|
@ -3865,7 +3864,8 @@ export class Node {
|
|||
|
||||
const anchor = this.editor.options.modalAnchor || DEFAULT_MODAL_ANCHOR
|
||||
const json = node.getValue()
|
||||
showTransformModal(anchor, json, query => {
|
||||
const { createQuery, executeQuery } = this.editor.options
|
||||
showTransformModal(anchor, json, createQuery, executeQuery, query => {
|
||||
node.transform(query)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import jmespath from 'jmespath'
|
||||
import { get, parsePath, parseString } from './util'
|
||||
|
||||
/**
|
||||
* Build a JMESPath query based on query options coming from the wizard
|
||||
* @param {JSON} json The JSON document for which to build the query.
|
||||
* Used for context information like determining
|
||||
* the type of values (string or number)
|
||||
* @param {QueryOptions} queryOptions
|
||||
* @return {string} Returns a query (as string)
|
||||
*/
|
||||
export function createQuery (json, queryOptions) {
|
||||
const { sort, filter, projection } = queryOptions
|
||||
let query = ''
|
||||
|
||||
if (!!filter) {
|
||||
const examplePath = filter.field !== '@'
|
||||
? ['0'].concat(parsePath('.' + filter.field))
|
||||
: ['0']
|
||||
const exampleValue = get(json, examplePath)
|
||||
const value1 = typeof exampleValue === 'string'
|
||||
? filter.value
|
||||
: parseString(filter.value)
|
||||
|
||||
query += '[? ' +
|
||||
filter.field + ' ' +
|
||||
filter.relation + ' ' +
|
||||
'`' + JSON.stringify(value1) + '`' +
|
||||
']'
|
||||
} else {
|
||||
query += Array.isArray(json)
|
||||
? '[*]'
|
||||
: '@'
|
||||
}
|
||||
|
||||
if (!!sort) {
|
||||
if (sort.direction === 'desc') {
|
||||
query += ' | reverse(sort_by(@, &' + sort.field + '))'
|
||||
} else {
|
||||
query += ' | sort_by(@, &' + sort.field + ')'
|
||||
}
|
||||
}
|
||||
|
||||
if (!!projection) {
|
||||
if (query[query.length - 1] !== ']') {
|
||||
query += ' | [*]'
|
||||
}
|
||||
|
||||
if (projection.fields.length === 1) {
|
||||
query += '.' + projection.fields[0]
|
||||
} else if (projection.fields.length > 1) {
|
||||
query += '.{' +
|
||||
projection.fields.map(value => {
|
||||
const parts = value.split('.')
|
||||
const last = parts[parts.length - 1]
|
||||
return last + ': ' + value
|
||||
}).join(', ') +
|
||||
'}'
|
||||
} else { // values.length === 0
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a JMESPath query
|
||||
* @param {JSON} json
|
||||
* @param {string} query
|
||||
* @return {JSON} Returns the transformed JSON
|
||||
*/
|
||||
export function executeQuery (json, query) {
|
||||
return jmespath.search(json, query)
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
'use strict'
|
||||
|
||||
import jmespath from 'jmespath'
|
||||
import { translate } from './i18n'
|
||||
import { ModeSwitcher } from './ModeSwitcher'
|
||||
import { ErrorTable } from './ErrorTable'
|
||||
|
@ -23,6 +22,7 @@ import {
|
|||
sortObjectKeys
|
||||
} from './util'
|
||||
import { History } from './History'
|
||||
import { createQuery, executeQuery } from './jmespathQuery'
|
||||
|
||||
const textmode = textModeMixins[0].mixin
|
||||
|
||||
|
@ -44,6 +44,8 @@ previewmode.create = function (container, options = {}) {
|
|||
options.mainMenuBar = options.mainMenuBar !== false
|
||||
options.enableSort = options.enableSort !== false
|
||||
options.enableTransform = options.enableTransform !== false
|
||||
options.createQuery = options.createQuery || createQuery
|
||||
options.executeQuery = options.executeQuery || executeQuery
|
||||
|
||||
this.options = options
|
||||
|
||||
|
@ -390,11 +392,13 @@ previewmode._showTransformModal = function () {
|
|||
this.executeWithBusyMessage(() => {
|
||||
const anchor = me.options.modalAnchor || DEFAULT_MODAL_ANCHOR
|
||||
const json = me.get()
|
||||
const { createQuery, executeQuery } = this.editor.options
|
||||
|
||||
me._renderPreview() // update array count
|
||||
|
||||
showTransformModal(anchor, json, query => {
|
||||
showTransformModal(anchor, json, createQuery, executeQuery, query => {
|
||||
me.executeWithBusyMessage(() => {
|
||||
const updatedJson = jmespath.search(json, query)
|
||||
const updatedJson = this.options.executeQuery(json, query)
|
||||
me._setAndFireOnChange(updatedJson)
|
||||
}, 'transforming...')
|
||||
})
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import jmespath from 'jmespath'
|
||||
import picoModal from 'picomodal'
|
||||
import Selectr from './assets/selectr/selectr'
|
||||
import { translate } from './i18n'
|
||||
import { stringifyPartial } from './jsonUtils'
|
||||
import { getChildPaths, get, parsePath, parseString, debounce } from './util'
|
||||
import { getChildPaths, debounce } from './util'
|
||||
import { MAX_PREVIEW_CHARACTERS } from './constants'
|
||||
|
||||
/**
|
||||
|
@ -11,10 +10,13 @@ import { MAX_PREVIEW_CHARACTERS } from './constants'
|
|||
* @param {HTMLElement} container The container where to center
|
||||
* the modal and create an overlay
|
||||
* @param {JSON} json The json data to be transformed
|
||||
* @param {function} createQuery Function called to create a query
|
||||
* from the wizard form
|
||||
* @param {function} executeQuery Execute a query for the preview pane
|
||||
* @param {function} onTransform Callback invoked with the created
|
||||
* query as callback
|
||||
*/
|
||||
export function showTransformModal (container, json, onTransform) {
|
||||
export function showTransformModal (container, json, createQuery, executeQuery, onTransform) {
|
||||
const value = json
|
||||
|
||||
const content = '<label class="pico-modal-contents">' +
|
||||
|
@ -176,7 +178,8 @@ export function showTransformModal (container, json, onTransform) {
|
|||
}
|
||||
}
|
||||
|
||||
query.value = Array.isArray(value) ? '[*]' : '@'
|
||||
// initialize with empty query
|
||||
query.value = createQuery(json, {})
|
||||
|
||||
function preprocessPath (path) {
|
||||
return (path === '')
|
||||
|
@ -187,68 +190,45 @@ export function showTransformModal (container, json, onTransform) {
|
|||
}
|
||||
|
||||
function generateQueryFromWizard () {
|
||||
if (filterField.value && filterRelation.value && filterValue.value) {
|
||||
const field1 = filterField.value
|
||||
const examplePath = field1 !== '@'
|
||||
? ['0'].concat(parsePath('.' + field1))
|
||||
: ['0']
|
||||
const exampleValue = get(value, examplePath)
|
||||
const value1 = typeof exampleValue === 'string'
|
||||
? filterValue.value
|
||||
: parseString(filterValue.value)
|
||||
const queryOptions = {}
|
||||
|
||||
query.value = '[? ' +
|
||||
field1 + ' ' +
|
||||
filterRelation.value + ' ' +
|
||||
'`' + JSON.stringify(value1) + '`' +
|
||||
']'
|
||||
} else {
|
||||
query.value = '[*]'
|
||||
if (filterField.value && filterRelation.value && filterValue.value) {
|
||||
queryOptions.filter = {
|
||||
field: filterField.value,
|
||||
relation: filterRelation.value,
|
||||
value: filterValue.value
|
||||
}
|
||||
}
|
||||
|
||||
if (sortField.value && sortOrder.value) {
|
||||
const field2 = sortField.value
|
||||
if (sortOrder.value === 'desc') {
|
||||
query.value += ' | reverse(sort_by(@, &' + field2 + '))'
|
||||
} else {
|
||||
query.value += ' | sort_by(@, &' + field2 + ')'
|
||||
queryOptions.sort = {
|
||||
field: sortField.value,
|
||||
direction: sortOrder.value
|
||||
}
|
||||
}
|
||||
|
||||
if (selectFields.value) {
|
||||
const values = []
|
||||
const fields = []
|
||||
for (let i = 0; i < selectFields.options.length; i++) {
|
||||
if (selectFields.options[i].selected) {
|
||||
const selectedValue = selectFields.options[i].value
|
||||
values.push(selectedValue)
|
||||
const selectedField = selectFields.options[i].value
|
||||
fields.push(selectedField)
|
||||
}
|
||||
}
|
||||
|
||||
if (query.value[query.value.length - 1] !== ']') {
|
||||
query.value += ' | [*]'
|
||||
}
|
||||
|
||||
if (values.length === 1) {
|
||||
query.value += '.' + values[0]
|
||||
} else if (values.length > 1) {
|
||||
query.value += '.{' +
|
||||
values.map(value => {
|
||||
const parts = value.split('.')
|
||||
const last = parts[parts.length - 1]
|
||||
return last + ': ' + value
|
||||
}).join(', ') +
|
||||
'}'
|
||||
} else { // values.length === 0
|
||||
// ignore
|
||||
queryOptions.projection = {
|
||||
fields
|
||||
}
|
||||
}
|
||||
|
||||
query.value = createQuery(json, queryOptions)
|
||||
|
||||
debouncedUpdatePreview()
|
||||
}
|
||||
|
||||
function updatePreview () {
|
||||
try {
|
||||
const transformed = jmespath.search(value, query.value)
|
||||
const transformed = executeQuery(value, query.value)
|
||||
|
||||
preview.className = 'jsoneditor-transform-preview'
|
||||
preview.value = stringifyPartial(transformed, 2, MAX_PREVIEW_CHARACTERS)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
import ace from './ace'
|
||||
import jmespath from 'jmespath'
|
||||
import { translate } from './i18n'
|
||||
import { ModeSwitcher } from './ModeSwitcher'
|
||||
import { ErrorTable } from './ErrorTable'
|
||||
|
@ -26,6 +25,7 @@ import {
|
|||
} from './util'
|
||||
import { DEFAULT_MODAL_ANCHOR } from './constants'
|
||||
import { tryRequireThemeJsonEditor } from './tryRequireThemeJsonEditor'
|
||||
import { createQuery, executeQuery } from './jmespathQuery'
|
||||
|
||||
// create a mixin with the functions for text mode
|
||||
const textmode = {}
|
||||
|
@ -47,6 +47,8 @@ textmode.create = function (container, options = {}) {
|
|||
options.mainMenuBar = options.mainMenuBar !== false
|
||||
options.enableSort = options.enableSort !== false
|
||||
options.enableTransform = options.enableTransform !== false
|
||||
options.createQuery = options.createQuery || createQuery
|
||||
options.executeQuery = options.executeQuery || executeQuery
|
||||
|
||||
this.options = options
|
||||
|
||||
|
@ -428,8 +430,9 @@ textmode._showTransformModal = function () {
|
|||
const me = this
|
||||
const anchor = this.options.modalAnchor || DEFAULT_MODAL_ANCHOR
|
||||
const json = this.get()
|
||||
showTransformModal(anchor, json, query => {
|
||||
const updatedJson = jmespath.search(json, query)
|
||||
const { createQuery, executeQuery } = this.editor.options
|
||||
showTransformModal(anchor, json, createQuery, executeQuery, query => {
|
||||
const updatedJson = this.options.executeQuery(json, query)
|
||||
me.set(updatedJson)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
} from './util'
|
||||
import { autocomplete } from './autocomplete'
|
||||
import { setLanguage, setLanguages, translate } from './i18n'
|
||||
import { createQuery, executeQuery } from './jmespathQuery'
|
||||
|
||||
// create a mixin with the functions for tree mode
|
||||
const treemode = {}
|
||||
|
@ -157,6 +158,8 @@ treemode._setOptions = function (options) {
|
|||
},
|
||||
timestampTag: true,
|
||||
timestampFormat: null,
|
||||
createQuery,
|
||||
executeQuery,
|
||||
onEvent: null,
|
||||
enableSort: true,
|
||||
enableTransform: true
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
/**
|
||||
* @typedef {object} QueryOptions
|
||||
* @property {FilterOptions} [filter]
|
||||
* @property {SortOptions} [sort]
|
||||
* @property {ProjectionOptions} [projection]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} FilterOptions
|
||||
* @property {string} field
|
||||
* @property {string} relation Can be '==', '<', etc
|
||||
* @property {string} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SortOptions
|
||||
* @property {string} field
|
||||
* @property {string} direction Can be 'asc' or 'desc'
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ProjectionOptions
|
||||
* @property {string[]} fields
|
||||
*/
|
Loading…
Reference in New Issue