import * as _ from 'lodash-es' import { last } from 'lodash-es' import { createLodashPropertySelector, createPropertySelector } from '../../utils/pathUtils.js' import { parseString } from '../../utils/stringUtils.js' import type { QueryLanguage, QueryLanguageOptions } from '../../types' import type { JSONValue } from 'immutable-json-patch' import { isInteger } from '../../utils/typeUtils.js' const description = `

Enter a JavaScript function to filter, sort, or transform the data. You can use Lodash functions like _.map, _.filter, _.orderBy, _.sortBy, _.groupBy, _.pick, _.uniq, _.get, etcetera.

` export const lodashQueryLanguage: QueryLanguage = { id: 'lodash', name: 'Lodash', description, createQuery, executeQuery } function createQuery(json: JSONValue, queryOptions: QueryLanguageOptions): string { const { filter, sort, projection } = queryOptions const queryParts = [] if (filter && filter.path && filter.relation && filter.value) { // Note that the comparisons embrace type coercion, // so a filter value like '5' (text) will match numbers like 5 too. const actualValueGetter = `item => item${createPropertySelector(filter.path)}` const filterValue = parseString(filter.value) const filterValueStr = typeof filterValue === 'string' ? `'${filter.value}'` : isInteger(filter.value) && !Number.isSafeInteger(filterValue) ? `${filter.value}n` // bigint : filter.value queryParts.push( ` data = _.filter(data, ${actualValueGetter} ${filter.relation} ${filterValueStr})\n` ) } if (sort && sort.path && sort.direction) { queryParts.push( ` data = _.orderBy(data, [${createLodashPropertySelector(sort.path)}], ['${ sort.direction }'])\n` ) } if (projection && projection.paths) { // It is possible to make a util function "pickFlat" // and use that when building the query to make it more readable. if (projection.paths.length > 1) { // Note that we do not use _.pick() here because this function doesn't flatten the results const paths = projection.paths.map((path) => { const name = last(path) || 'item' // 'item' in case of having selected the whole item return ` ${JSON.stringify(name)}: item${createPropertySelector(path)}` }) queryParts.push(` data = _.map(data, item => ({\n${paths.join(',\n')}\n }))\n`) } else { const path = projection.paths[0] queryParts.push(` data = _.map(data, item => item${createPropertySelector(path)})\n`) } } queryParts.push(' return data\n') return `function query (data) {\n${queryParts.join('')}}` } function executeQuery(json: JSONValue, query: string): JSONValue { // 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";\n' + '\n' + query + '\n' + '\n' + 'if (typeof query !== "function") {\n' + ' throw new Error("Cannot execute query: expecting a function named \'query\' but is undefined")\n' + '}\n' + '\n' + 'return query;\n' )(_) const output = queryFn(json) return output !== undefined ? output : null }