diff --git a/src/lib/plugins/query/javascriptQueryLanguage.js b/src/lib/plugins/query/javascriptQueryLanguage.js index 3634e6b7..b58a0b3e 100644 --- a/src/lib/plugins/query/javascriptQueryLanguage.js +++ b/src/lib/plugins/query/javascriptQueryLanguage.js @@ -14,11 +14,11 @@ export const javascriptQueryLanguage = { } /** - * Turn a path like + * Turn a path like: * * ['location', 'latitude'] * - * into a JavaScript selector + * into a JavaScript selector (string) like: * * '?.["location"]?.["latitude"]' * diff --git a/src/lib/plugins/query/javascriptQueryLanguage.test.js b/src/lib/plugins/query/javascriptQueryLanguage.test.js new file mode 100644 index 00000000..5dc43b74 --- /dev/null +++ b/src/lib/plugins/query/javascriptQueryLanguage.test.js @@ -0,0 +1,201 @@ +import assert from 'assert' +import { javascriptQueryLanguage } from './javascriptQueryLanguage.js' +import { cloneDeep } from 'lodash-es' + +const { createQuery, executeQuery } = javascriptQueryLanguage + +const user1 = { _id: '1', user: { name: 'Stuart', age: 6 } } +const user3 = { _id: '3', user: { name: 'Kevin', age: 8 } } +const user2 = { _id: '2', user: { name: 'Bob', age: 7 } } + +const users = [user1, user3, user2] +const originalUsers = cloneDeep([user1, user3, user2]) + +describe('javascriptQueryLanguage', () => { + describe('createQuery and executeQuery', () => { + it('should create a and execute an empty query', () => { + const query = createQuery(users, {}) + const result = executeQuery(users, query) + assert.deepStrictEqual(query, 'function query (data) {\n return data\n}') + assert.deepStrictEqual(result, users) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a filter query for a nested property', () => { + const query = createQuery(users, { + filter: { + field: ['user', 'name'], + relation: '==', + value: 'Bob' + } + }) + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = data.filter(item => item?.["user"]?.["name"] == \'Bob\')\n' + + ' return data\n' + + '}' + ) + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, [user2]) + assert.deepStrictEqual(users, originalUsers) // must not touch the original data + }) + + it('should create and execute a filter query for the whole array item', () => { + const data = [2, 3, 1] + const originalData = cloneDeep(data) + const query = createQuery(data, { + filter: { + field: [], + relation: '==', + value: '1' + } + }) + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + " data = data.filter(item => item == '1')\n" + + ' return data\n' + + '}' + ) + + const result = executeQuery(data, query) + assert.deepStrictEqual(result, [1]) + assert.deepStrictEqual(data, originalData) // must not touch the original data + }) + + it('should create and execute a sort query in ascending direction', () => { + const query = createQuery(users, { + sort: { + field: ['user', 'age'], + direction: 'asc' + } + }) + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = data.slice().sort((a, b) => {\n' + + ' // sort ascending\n' + + ' const valueA = a?.["user"]?.["age"]\n' + + ' const valueB = b?.["user"]?.["age"]\n' + + ' return valueA > valueB ? 1 : valueA < valueB ? -1 : 0\n' + + ' })\n' + + ' return data\n' + + '}' + ) + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, [user1, user2, user3]) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a sort query in descending direction', () => { + const query = createQuery(users, { + sort: { + field: ['user', 'age'], + direction: 'desc' + } + }) + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = data.slice().sort((a, b) => {\n' + + ' // sort descending\n' + + ' const valueA = a?.["user"]?.["age"]\n' + + ' const valueB = b?.["user"]?.["age"]\n' + + ' return valueA > valueB ? -1 : valueA < valueB ? 1 : 0\n' + + ' })\n' + + ' return data\n' + + '}' + ) + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, [user3, user2, user1]) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a project query for a single property', () => { + const query = createQuery(users, { + projection: { + fields: [['user', 'name']] + } + }) + + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = data.map(item => item?.["user"]?.["name"])\n' + + ' return data\n' + + '}' + ) + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, ['Stuart', 'Kevin', 'Bob']) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a project query for a multiple properties', () => { + const query = createQuery(users, { + projection: { + fields: [['user', 'name'], ['_id']] + } + }) + + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = data.map(item => ({\n' + + ' "name": item?.["user"]?.["name"],\n' + + ' "_id": item?.["_id"]})\n' + + ' )\n' + + ' return data\n' + + '}' + ) + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, [ + { name: 'Stuart', _id: '1' }, + { name: 'Kevin', _id: '3' }, + { name: 'Bob', _id: '2' } + ]) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a query with filter, sort and project', () => { + const query = createQuery(users, { + filter: { + field: ['user', 'age'], + relation: '<=', + value: '7' + }, + sort: { + field: ['user', 'name'], + direction: 'asc' + }, + projection: { + fields: [['user', 'name']] + } + }) + + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = data.filter(item => item?.["user"]?.["age"] <= \'7\')\n' + + ' data = data.slice().sort((a, b) => {\n' + + ' // sort ascending\n' + + ' const valueA = a?.["user"]?.["name"]\n' + + ' const valueB = b?.["user"]?.["name"]\n' + + ' return valueA > valueB ? 1 : valueA < valueB ? -1 : 0\n' + + ' })\n' + + ' data = data.map(item => item?.["user"]?.["name"])\n' + + ' return data\n' + + '}' + ) + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, ['Bob', 'Stuart']) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + }) +}) diff --git a/src/lib/plugins/query/jmespathQueryLanguage.js b/src/lib/plugins/query/jmespathQueryLanguage.js index 39042d93..a463f52c 100644 --- a/src/lib/plugins/query/jmespathQueryLanguage.js +++ b/src/lib/plugins/query/jmespathQueryLanguage.js @@ -28,26 +28,34 @@ export const jmespathQueryLanguage = { * @param {QueryLanguageOptions} queryOptions * @return {string} Returns a query (as string) */ -export function createQuery(json, queryOptions) { +function createQuery(json, queryOptions) { const { sort, filter, projection } = queryOptions let query = '' if (filter) { - const examplePath = filter.field !== '@' ? ['0'].concat(parsePath('.' + filter.field)) : ['0'] + const examplePath = ['0'].concat(filter.field) const exampleValue = getIn(json, examplePath) const value1 = typeof exampleValue === 'string' ? filter.value : parseString(filter.value) query += - '[? ' + filter.field + ' ' + filter.relation + ' ' + '`' + JSON.stringify(value1) + '`' + ']' + '[? ' + + stringifyPathForJmespath(filter.field) + + ' ' + + filter.relation + + ' ' + + '`' + + JSON.stringify(value1) + + '`' + + ']' } else { query += Array.isArray(json) ? '[*]' : '@' } if (sort) { if (sort.direction === 'desc') { - query += ' | reverse(sort_by(@, &' + sort.field + '))' + query += ' | reverse(sort_by(@, &' + stringifyPathForJmespath(sort.field) + '))' } else { - query += ' | sort_by(@, &' + sort.field + ')' + query += ' | sort_by(@, &' + stringifyPathForJmespath(sort.field) + ')' } } @@ -57,15 +65,14 @@ export function createQuery(json, queryOptions) { } if (projection.fields.length === 1) { - query += '.' + projection.fields[0] + query += '.' + stringifyPathForJmespath(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 + .map((field) => { + const name = field[field.length - 1] + return name + ': ' + stringifyPathForJmespath(field) }) .join(', ') + '}' @@ -84,87 +91,10 @@ export function createQuery(json, queryOptions) { * @param {string} query * @return {JSON} Returns the transformed JSON */ -export function executeQuery(json, query) { +function executeQuery(json, query) { return jmespath.search(json, query) } -// TODO: move parsePath to pathUtils.js? -/** - * Parse a JSON path like '.items[3].name' into an array - * @param {string} jsonPath - * @return {Array} - */ -export function parsePath(jsonPath) { - const path = [] - let i = 0 - - function parseProperty() { - let prop = '' - while (jsonPath[i] !== undefined && /[\w$]/.test(jsonPath[i])) { - prop += jsonPath[i] - i++ - } - - if (prop === '') { - throw new Error('Invalid JSON path: property name expected at index ' + i) - } - - return prop - } - - function parseIndex(end) { - let name = '' - while (jsonPath[i] !== undefined && jsonPath[i] !== end) { - name += jsonPath[i] - i++ - } - - if (jsonPath[i] !== end) { - throw new Error('Invalid JSON path: unexpected end, character ' + end + ' expected') - } - - return name - } - - while (jsonPath[i] !== undefined) { - if (jsonPath[i] === '.') { - i++ - path.push(parseProperty()) - } else if (jsonPath[i] === '[') { - i++ - - if (jsonPath[i] === "'" || jsonPath[i] === '"') { - const end = jsonPath[i] - i++ - - path.push(parseIndex(end)) - - if (jsonPath[i] !== end) { - throw new Error("Invalid JSON path: closing quote ' expected at index " + i) - } - i++ - } else { - let index = parseIndex(']').trim() - if (index.length === 0) { - throw new Error('Invalid JSON path: array value expected at index ' + i) - } - // Coerce numeric indices to numbers, but ignore star - index = index === '*' ? index : JSON.parse(index) - path.push(index) - } - - if (jsonPath[i] !== ']') { - throw new Error('Invalid JSON path: closing bracket ] expected at index ' + i) - } - i++ - } else { - throw new Error('Invalid JSON path: unexpected character "' + jsonPath[i] + '" at index ' + i) - } - } - - return path -} - /** * Cast contents of a string to the correct type. * This can be a string, a number, a boolean, etc @@ -196,3 +126,31 @@ export function parseString(str) { return str } + +/** + * @param {string[]} path + * @returns {string} + */ +// TODO: unit test stringifyPathForJmespath +// TODO: Isn't there a helper function exposed by the JMESPath library? +export function stringifyPathForJmespath(path) { + if (path.length === 0) { + return '@' + } + + const str = path + .map((prop) => { + if (typeof prop === 'number') { + return '[' + prop + ']' + } else if (typeof prop === 'string' && prop.match(/^[A-Za-z0-9_$]+$/)) { + return '.' + prop + } else { + return '."' + prop + '"' + } + }) + .join('') + + return str[0] === '.' + ? str.slice(1) // remove first dot + : str +} diff --git a/src/lib/plugins/query/jmespathQueryLanguage.test.js b/src/lib/plugins/query/jmespathQueryLanguage.test.js index 031d34ec..8ee96fe9 100644 --- a/src/lib/plugins/query/jmespathQueryLanguage.test.js +++ b/src/lib/plugins/query/jmespathQueryLanguage.test.js @@ -1,53 +1,142 @@ import assert from 'assert' -import { parsePath, parseString } from './jmespathQueryLanguage.js' +import { jmespathQueryLanguage, parseString } from './jmespathQueryLanguage.js' +import { cloneDeep } from 'lodash-es' + +const { createQuery, executeQuery } = jmespathQueryLanguage describe('jmespathQueryLanguage', () => { - // TODO: write tests for createQuery - // TODO: write tests for executeQuery - - describe('jsonPath', () => { - it('should parse a json path', () => { - assert.deepStrictEqual(parsePath(''), []) - assert.deepStrictEqual(parsePath('.foo'), ['foo']) - assert.deepStrictEqual(parsePath('.foo.bar'), ['foo', 'bar']) - assert.deepStrictEqual(parsePath('.foo[2]'), ['foo', 2]) - assert.deepStrictEqual(parsePath('.foo[2].bar'), ['foo', 2, 'bar']) - assert.deepStrictEqual(parsePath('.foo["prop with spaces"]'), ['foo', 'prop with spaces']) - assert.deepStrictEqual( - parsePath(".foo['prop with single quotes as outputted by ajv library']"), - ['foo', 'prop with single quotes as outputted by ajv library'] - ) - assert.deepStrictEqual(parsePath('.foo["prop with . dot"]'), ['foo', 'prop with . dot']) - assert.deepStrictEqual(parsePath('.foo["prop with ] character"]'), [ - 'foo', - 'prop with ] character' + describe('createQuery and executeQuery', () => { + const user1 = { _id: '1', user: { name: 'Stuart', age: 6 } } + const user3 = { _id: '3', user: { name: 'Kevin', age: 8 } } + const user2 = { _id: '2', user: { name: 'Bob', age: 7 } } + + const users = [user1, user3, user2] + const originalUsers = cloneDeep([user1, user3, user2]) + + it('should create a and execute an empty query', () => { + const query = createQuery(users, {}) + const result = executeQuery(users, query) + assert.deepStrictEqual(query, '[*]') + assert.deepStrictEqual(result, users) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a filter query for a nested property', () => { + const query = createQuery(users, { + filter: { + field: ['user', 'name'], + relation: '==', + value: 'Bob' + } + }) + assert.deepStrictEqual(query, '[? user.name == `"Bob"`]') + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, [user2]) + assert.deepStrictEqual(users, originalUsers) // must not touch the original data + }) + + it('should create and execute a filter query for the whole array item', () => { + const data = [2, 3, 1] + const originalData = cloneDeep(data) + + const query = createQuery(data, { + filter: { + field: [], + relation: '==', + value: '1' + } + }) + assert.deepStrictEqual(query, '[? @ == `1`]') + + const result = executeQuery(data, query) + assert.deepStrictEqual(result, [1]) + assert.deepStrictEqual(data, originalData) // must not touch the original data + }) + + it('should create and execute a sort query in ascending direction', () => { + const query = createQuery(users, { + sort: { + field: ['user', 'age'], + direction: 'asc' + } + }) + assert.deepStrictEqual(query, '[*] | sort_by(@, &user.age)') + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, [user1, user2, user3]) + + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a sort query in descending direction', () => { + const query = createQuery(users, { + sort: { + field: ['user', 'age'], + direction: 'desc' + } + }) + assert.deepStrictEqual(query, '[*] | reverse(sort_by(@, &user.age))') + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, [user3, user2, user1]) + + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a project query for a single property', () => { + const query = createQuery(users, { + projection: { + fields: [['user', 'name']] + } + }) + assert.deepStrictEqual(query, '[*].user.name') + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, ['Stuart', 'Kevin', 'Bob']) + + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a project query for a multiple properties', () => { + const query = createQuery(users, { + projection: { + fields: [['user', 'name'], ['_id']] + } + }) + assert.deepStrictEqual(query, '[*].{name: user.name, _id: _id}') + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, [ + { name: 'Stuart', _id: '1' }, + { name: 'Kevin', _id: '3' }, + { name: 'Bob', _id: '2' } ]) - assert.deepStrictEqual(parsePath('.foo[*].bar'), ['foo', '*', 'bar']) - assert.deepStrictEqual(parsePath('[2]'), [2]) + + assert.deepStrictEqual(users, originalUsers) // must not touch the original users }) - it('should throw an exception in case of an invalid path', () => { - assert.throws(() => { - parsePath('.') - }, /Invalid JSON path: property name expected at index 1/) - assert.throws(() => { - parsePath('[') - }, /Invalid JSON path: unexpected end, character ] expected/) - assert.throws(() => { - parsePath('[]') - }, /Invalid JSON path: array value expected at index 1/) - assert.throws(() => { - parsePath('.foo[ ]') - }, /Invalid JSON path: array value expected at index 7/) - assert.throws(() => { - parsePath('.[]') - }, /Invalid JSON path: property name expected at index 1/) - assert.throws(() => { - parsePath('["23]') - }, /Invalid JSON path: unexpected end, character " expected/) - assert.throws(() => { - parsePath('.foo bar') - }, /Invalid JSON path: unexpected character " " at index 4/) + it('should create and execute a query with filter, sort and project', () => { + const query = createQuery(users, { + filter: { + field: ['user', 'age'], + relation: '<=', + value: '7' + }, + sort: { + field: ['user', 'name'], + direction: 'asc' + }, + projection: { + fields: [['user', 'name']] + } + }) + assert.deepStrictEqual(query, '[? user.age <= `7`] | sort_by(@, &user.name) | [*].user.name') + + const result = executeQuery(users, query) + assert.deepStrictEqual(result, ['Bob', 'Stuart']) + + assert.deepStrictEqual(users, originalUsers) // must not touch the original users }) }) diff --git a/src/lib/plugins/query/lodashQueryLanguage.js b/src/lib/plugins/query/lodashQueryLanguage.js index f8327992..746a87ac 100644 --- a/src/lib/plugins/query/lodashQueryLanguage.js +++ b/src/lib/plugins/query/lodashQueryLanguage.js @@ -44,7 +44,7 @@ function createQuery(json, queryOptions) { if (sort) { queryParts.push( - ` data = _.orderBy(data, ${JSON.stringify(sort.field)}, '${sort.direction}')\n` + ` data = _.orderBy(data, [${JSON.stringify(sort.field)}], ['${sort.direction}'])\n` ) } diff --git a/src/lib/plugins/query/lodashQueryLanguage.test.js b/src/lib/plugins/query/lodashQueryLanguage.test.js new file mode 100644 index 00000000..a3fb8bed --- /dev/null +++ b/src/lib/plugins/query/lodashQueryLanguage.test.js @@ -0,0 +1,184 @@ +import assert from 'assert' +import { lodashQueryLanguage } from './lodashQueryLanguage.js' +import { cloneDeep } from 'lodash-es' + +const { createQuery, executeQuery } = lodashQueryLanguage + +const user1 = { _id: '1', user: { name: 'Stuart', age: 6 } } +const user3 = { _id: '3', user: { name: 'Kevin', age: 8 } } +const user2 = { _id: '2', user: { name: 'Bob', age: 7 } } + +const users = [user1, user3, user2] +const originalUsers = cloneDeep([user1, user3, user2]) + +describe('lodashQueryLanguage', () => { + describe('createQuery and executeQuery', () => { + it('should create a and execute an empty query', () => { + const query = createQuery(users, {}) + const result = executeQuery(users, query) + assert.deepStrictEqual(query, 'function query (data) {\n return data\n}') + assert.deepStrictEqual(result, users) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a filter query for a nested property', () => { + assert.deepStrictEqual( + createQuery( + {}, + { + filter: { + field: ['user', 'name'], + relation: '==', + value: 'Bob' + } + } + ), + 'function query (data) {\n' + + ' data = data.filter(item => _.get(item, ["user","name"]) == \'Bob\')\n' + + ' return data\n' + + '}' + ) + }) + + it('should create and execute a filter query for the whole array item', () => { + const data = [2, 3, 1] + const originalData = cloneDeep(data) + const query = createQuery(data, { + filter: { + field: [], + relation: '==', + value: '1' + } + }) + const result = executeQuery(data, query) + + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + " data = data.filter(item => item == '1')\n" + + ' return data\n' + + '}' + ) + + assert.deepStrictEqual(result, [1]) + assert.deepStrictEqual(data, originalData) // must not touch the original data + }) + + it('should create and execute a sort query in ascending direction', () => { + const query = createQuery(users, { + sort: { + field: ['user', 'age'], + direction: 'asc' + } + }) + const result = executeQuery(users, query) + + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = _.orderBy(data, [["user","age"]], [\'asc\'])\n' + + ' return data\n' + + '}' + ) + assert.deepStrictEqual(result, [user1, user2, user3]) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a sort query in descending direction', () => { + const query = createQuery(users, { + sort: { + field: ['user', 'age'], + direction: 'desc' + } + }) + const result = executeQuery(users, query) + + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = _.orderBy(data, [["user","age"]], [\'desc\'])\n' + + ' return data\n' + + '}' + ) + assert.deepStrictEqual(result, [user3, user2, user1]) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a project query for a single property', () => { + const query = createQuery(users, { + projection: { + fields: [['user', 'name']] + } + }) + const result = executeQuery(users, query) + + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = data.map(item => _.get(item, ["user","name"]))\n' + + ' return data\n' + + '}' + ) + assert.deepStrictEqual(result, ['Stuart', 'Kevin', 'Bob']) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a project query for a multiple properties', () => { + const query = createQuery(users, { + projection: { + fields: [['user', 'name'], ['_id']] + } + }) + const result = executeQuery(users, query) + + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = data.map(item => ({\n' + + ' "name": _.get(item, ["user","name"]),\n' + + ' "_id": _.get(item, ["_id"])})\n' + + ' )\n' + + ' return data\n' + + '}' + ) + + assert.deepStrictEqual(result, [ + { name: 'Stuart', _id: '1' }, + { name: 'Kevin', _id: '3' }, + { name: 'Bob', _id: '2' } + ]) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + + it('should create and execute a query with filter, sort and project', () => { + const query = createQuery(users, { + filter: { + field: ['user', 'age'], + relation: '<=', + value: '7' + }, + sort: { + field: ['user', 'name'], + direction: 'asc' + }, + projection: { + fields: [['user', 'name']] + } + }) + const result = executeQuery(users, query) + + assert.deepStrictEqual( + query, + 'function query (data) {\n' + + ' data = data.filter(item => _.get(item, ["user","age"]) <= \'7\')\n' + + ' data = _.orderBy(data, [["user","name"]], [\'asc\'])\n' + + ' data = data.map(item => _.get(item, ["user","name"]))\n' + + ' return data\n' + + '}' + ) + + assert.deepStrictEqual(result, ['Bob', 'Stuart']) + assert.deepStrictEqual(users, originalUsers) // must not touch the original users + }) + }) +}) diff --git a/src/lib/types.js b/src/lib/types.js index 794f0cc4..7fe7e1e1 100644 --- a/src/lib/types.js +++ b/src/lib/types.js @@ -187,15 +187,15 @@ /** * @typedef {Object} QueryLanguageOptions * @property {{ - * field: Path, + * field: string[], * relation: '==' | '!=' | '<' | '<=' | '>' | '>=', * value: string - * }} filter + * }} [filter] * @property {{ - * field: Path, + * field: string[], * direction: 'asc' | 'desc' - * }} sort + * }} [sort] * @property {{ - * fields: Path[] - * }} projection + * fields: string[][] + * }} [projection] */ diff --git a/src/lib/utils/pathUtils.js b/src/lib/utils/pathUtils.js index fd082bec..62047ec5 100644 --- a/src/lib/utils/pathUtils.js +++ b/src/lib/utils/pathUtils.js @@ -10,7 +10,13 @@ export function stringifyPath(path) { return path .map((prop) => { - return typeof prop === 'number' ? `[${prop}]` : `.${prop}` + if (typeof prop === 'number') { + return '[' + prop + ']' + } else if (typeof prop === 'string' && prop.match(/^[A-Za-z0-9_$]+$/)) { + return '.' + prop + } else { + return '["' + prop + '"]' + } }) .join('') } diff --git a/src/lib/utils/pathUtils.test.js b/src/lib/utils/pathUtils.test.js index 4e9d1117..b6ca49e6 100644 --- a/src/lib/utils/pathUtils.test.js +++ b/src/lib/utils/pathUtils.test.js @@ -3,8 +3,15 @@ import { stringifyPath } from './pathUtils.js' describe('pathUtils', () => { it('stringifyPath', () => { - assert.strictEqual(stringifyPath(['data', 2, 'nested', 'property']), '.data[2].nested.property') - assert.strictEqual(stringifyPath(['']), '.') assert.strictEqual(stringifyPath([]), '') + assert.strictEqual(stringifyPath(['']), '[""]') + assert.strictEqual(stringifyPath(['foo']), '.foo') + assert.strictEqual(stringifyPath(['foo', 'bar']), '.foo.bar') + assert.strictEqual(stringifyPath(['foo', 2]), '.foo[2]') + assert.strictEqual(stringifyPath(['foo', 2, 'bar']), '.foo[2].bar') + assert.strictEqual(stringifyPath(['foo', 2, 'bar_baz']), '.foo[2].bar_baz') + assert.strictEqual(stringifyPath([2]), '[2]') + assert.strictEqual(stringifyPath(['foo', 'prop-with-hyphens']), '.foo["prop-with-hyphens"]') + assert.strictEqual(stringifyPath(['foo', 'prop with spaces']), '.foo["prop with spaces"]') }) })