diff --git a/HISTORY.md b/HISTORY.md index 6770b64..0fcaa20 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,6 +3,14 @@ https://github.com/josdejong/jsoneditor +## not yet published, version 8.5.0 + +- Implemented support for customizing the query language used in the + Transform modal. New options `createQuery`, `executeQuery`, and + `queryDescription` are available for this now. An example is available + in `examples/23_custom_query_language.html`. See #857, #871. + + ## 2020-01-25, version 8.4.1 - Fix `console.log` in production code. Oopsie. diff --git a/docs/api.md b/docs/api.md index c2d1317..6310649 100644 --- a/docs/api.md +++ b/docs/api.md @@ -589,6 +589,55 @@ Constructs a new JSONEditor. - `{Number} maxVisibleChilds` Number of children allowed for a given node before the "show more / show all" message appears (in 'tree', 'view', or 'form' modes). `100` by default. + +- `{ function(json: JSON, queryOptions: QueryOptions) -> string } createQuery` + + Create a query string based on query options filled in the Transform Wizard in the Transform modal. + Normally used in combination with `executeQuery`. + The input for the function are the entered query options and the current JSON, and the output + must be a string containing the query. This query will be executed using `executeQuery`. + + The query options have the following structure: + + ``` + interface QueryOptions { + filter?: { + field: string | '@' + relation: '==' | '!=' | '<' | '<=' | '>' | '>=' + value: string + } + sort?: { + field: string | '@' + direction: 'asc' | 'desc' + } + projection?: { + fields: string[] + } + } + ``` + + Note that there is a special case `'@'` for `filter.field` and `sort.field`. + It means that the field itself is selected, for example when having an array containing numbers. + + A usage example can be found in `examples/23_custom_query_language.html`. + + +- `{ function(json: JSON, query: string) -> JSON } executeQuery` + + Replace the build-in query language used in the Transform modal with a custom language. + Normally used in combination with `createQuery`. + The input for the function is the current JSON and a query string, and output must be the transformed JSON. + + A usage example can be found in `examples/23_custom_query_language.html`. + +- `{string} queryDescription` + + A text description displayed on top of the Transform modal. + Can be used to explain a custom query language implemented via `createQuery` and `executeQuery`. + The text can contain HTML code like a link to a web page. + + A usage example can be found in `examples/23_custom_query_language.html`. + ### Methods diff --git a/examples/23_custom_query_language.html b/examples/23_custom_query_language.html new file mode 100644 index 0000000..33c21e2 --- /dev/null +++ b/examples/23_custom_query_language.html @@ -0,0 +1,123 @@ + + + + JSONEditor | Custom query language + + + + + + + + +

+ This demo shows how to configure a custom query language. + Click on the "Transform" button and try it out. +

+

+ This basic example uses lodash functions filter, sort, and pick, + but you can run any JavaScript code. +

+

+ WARNING: this example uses new Function() which can be dangerous when executed with arbitrary code. + Don't use it in production. +

+
+ + + + diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js index 6fd4482..305b3a3 100644 --- a/src/js/JSONEditor.js +++ b/src/js/JSONEditor.js @@ -184,7 +184,8 @@ JSONEditor.VALID_OPTIONS = [ 'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation', 'sortObjectKeys', 'navigationBar', 'statusBar', 'mainMenuBar', 'languages', 'language', 'enableSort', 'enableTransform', 'maxVisibleChilds', 'onValidationError', - 'modalAnchor', 'popupAnchor' + 'modalAnchor', 'popupAnchor', + 'createQuery', 'executeQuery', 'queryDescription' ] /** diff --git a/src/js/Node.js b/src/js/Node.js index af85dfa..98366fe 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -1,6 +1,5 @@ 'use strict' -import jmespath from 'jmespath' import naturalSort from 'javascript-natural-sort' import { createAbsoluteAnchor } from './createAbsoluteAnchor' import { ContextMenu } from './ContextMenu' @@ -3331,7 +3330,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() @@ -3883,12 +3882,16 @@ export class Node { * Show transform modal */ showTransformModal () { - const node = this + const { modalAnchor, createQuery, executeQuery, queryDescription } = this.editor.options + const json = this.getValue() - const anchor = this.editor.options.modalAnchor || DEFAULT_MODAL_ANCHOR - const json = node.getValue() - showTransformModal(anchor, json, query => { - node.transform(query) + showTransformModal({ + anchor: modalAnchor || DEFAULT_MODAL_ANCHOR, + json, + queryDescription, // can be undefined + createQuery, + executeQuery, + onTransform: query => { this.transform(query) } }) } diff --git a/src/js/jmespathQuery.js b/src/js/jmespathQuery.js new file mode 100644 index 0000000..a9f6b23 --- /dev/null +++ b/src/js/jmespathQuery.js @@ -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) +} diff --git a/src/js/previewmode.js b/src/js/previewmode.js index dd74985..7b3c6be 100644 --- a/src/js/previewmode.js +++ b/src/js/previewmode.js @@ -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 @@ -385,18 +387,24 @@ previewmode._showSortModal = function () { * @private */ previewmode._showTransformModal = function () { - const me = this - this.executeWithBusyMessage(() => { - const anchor = me.options.modalAnchor || DEFAULT_MODAL_ANCHOR - const json = me.get() - me._renderPreview() // update array count + const { createQuery, executeQuery, modalAnchor, queryDescription } = this.options + const json = this.get() - showTransformModal(anchor, json, query => { - me.executeWithBusyMessage(() => { - const updatedJson = jmespath.search(json, query) - me._setAndFireOnChange(updatedJson) - }, 'transforming...') + this._renderPreview() // update array count + + showTransformModal({ + anchor: modalAnchor || DEFAULT_MODAL_ANCHOR, + json, + queryDescription, // can be undefined + createQuery, + executeQuery, + onTransform: query => { + this.executeWithBusyMessage(() => { + const updatedJson = executeQuery(json, query) + this._setAndFireOnChange(updatedJson) + }, 'transforming...') + } }) }, 'parsing...') } diff --git a/src/js/showTransformModal.js b/src/js/showTransformModal.js index 2482b1f..5136c82 100644 --- a/src/js/showTransformModal.js +++ b/src/js/showTransformModal.js @@ -1,28 +1,43 @@ -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' +const DEFAULT_DESCRIPTION = + 'Enter a JMESPath query to filter, sort, or transform the JSON data.
' + + 'To learn JMESPath, go to the interactive tutorial.' + /** * Show advanced filter and transform modal using JMESPath - * @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} onTransform Callback invoked with the created - * query as callback + * @param {Object} params + * @property {HTMLElement} container The container where to center + * the modal and create an overlay + * @property {JSON} json The json data to be transformed + * @property {string} [queryDescription] Optional custom description explaining + * the transform functionality + * @property {function} createQuery Function called to create a query + * from the wizard form + * @property {function} executeQuery Execute a query for the preview pane + * @property {function} onTransform Callback invoked with the created + * query as callback */ -export function showTransformModal (container, json, onTransform) { +export function showTransformModal ( + { + container, + json, + queryDescription = DEFAULT_DESCRIPTION, + createQuery, + executeQuery, + onTransform + } +) { const value = json const content = '