From e777eed00dd19bc57006cc4619a4f80d8eb9b72e Mon Sep 17 00:00:00 2001 From: Ajai Shankar Date: Sun, 12 Feb 2023 17:27:54 -0600 Subject: [PATCH 1/5] feat(get): supercharged res getter --- packages/bruno-js/src/get.js | 128 ++++++++++++++++++++++++++++ packages/bruno-js/src/utils.js | 22 +++++ packages/bruno-js/tests/get.spec.js | 46 ++++++++++ 3 files changed, 196 insertions(+) create mode 100644 packages/bruno-js/src/get.js create mode 100644 packages/bruno-js/tests/get.spec.js diff --git a/packages/bruno-js/src/get.js b/packages/bruno-js/src/get.js new file mode 100644 index 000000000..4a1e490f0 --- /dev/null +++ b/packages/bruno-js/src/get.js @@ -0,0 +1,128 @@ +/** + * Gets property values for all items in source array + */ +const arrayGet = (source, prop) => { + if (!Array.isArray(source)) return []; + + const results = []; + + source.forEach(item => { + const value = item[prop]; + if (value != null) { + results.push(...Array.isArray(value) ? value : [value]); + } + }); + + return results; +}; + +/** + * Recursively collects property values into results + */ +const deepGet = (source, prop, results) => { + if (source == null || typeof source !== 'object') return; + + if (Array.isArray(source)) { + source.forEach(item => deepGet(item, prop, results)); + } else { + for (const key in source) { + if (key === prop) { + const value = source[prop]; + results.push(...Array.isArray(value) ? value : [value]); + } else { + deepGet(source[key], prop, results); + } + } + } +}; + +/** + * Gets property value(s) from source + */ +const baseGet = (source, prop, deep = false) => { + if (source == null || typeof source !== 'object') return; + + if (!deep) { + return Array.isArray(source) ? arrayGet(source, prop) : source[prop]; + } else { + const results = []; + deepGet(source, prop, results); + return results.filter(value => value != null); + } +}; + +/** + * Apply filter on source array or object + */ +const applyFilter = (source, predicate, single = false) => { + const list = Array.isArray(source) ? source : [source]; + const result = list.filter(predicate); + return single ? result[0] : result; +}; + +/** + * Supercharged getter with deep navigation and filter support + * + * 1. Easy array navigation + * ```js + * get(data, 'customer.orders.items.amount') + * ``` + * 2. Deep navigation .. double dots + * ```js + * get(data, '..items.amount') + * ``` + * 3. Array indexing + * ```js + * get(data, '..items[0].amount') + * ``` + * 4. Array filtering [?] with corresponding js filter + * ```js + * get(data, '..items[?].amount', i => i.amount > 20) + * ``` + */ +function get(source, path, ...filters) { + const paths = path + .replace(/\s+/g, '') + .split(/(\.{1,2}|\[\?\]|\[\d+\])/g) // ["..", "items", "[?]", ".", "amount", "[0]" ] + .filter(s => s.length > 0) + .map(str => { + str = str.replace(/\[|\]/g, ''); + const index = parseInt(str); + return isNaN(index) ? str : index; + }); + + let index = 0, lookbehind = '', filterIndex = 0; + + while (source != null && index < paths.length) { + const token = paths[index++]; + + switch (true) { + case token === "..": + case token === ".": + break; + case token === "?": + const filter = filters[filterIndex++]; + if (filter == null) + throw new Error(`missing filter for ${lookbehind}`); + const single = !Array.isArray(source); + source = applyFilter(source, filter, single); + break; + case typeof token === 'number': + source = source[token]; + break; + default: + source = baseGet(source, token, lookbehind === ".."); + if (Array.isArray(source) && !source.length) { + source = undefined; + } + } + + lookbehind = token; + } + + return source; +} + +module.exports = { + get +}; \ No newline at end of file diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 6bc73da83..151bb6014 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -1,4 +1,5 @@ const jsonQuery = require('json-query'); +const { get } = require("./get"); const JS_KEYWORDS = ` break case catch class const continue debugger default delete do @@ -70,6 +71,27 @@ const createResponseParser = (response = {}) => { res.headers = response.headers; res.body = response.data; + /** + * Get supports deep object navigation and filtering + * 1. Easy array navigation + * ```js + * res.get('customer.orders.items.amount') + * ``` + * 2. Deep navigation .. double dots + * ```js + * res.get('..items.amount') + * ``` + * 3. Array indexing + * ```js + * res.get('..items[0].amount') + * ``` + * 4. Array filtering [?] with corresponding js filter + * ```js + * res.get('..items[?].amount', i => i.amount > 20) + * ``` + */ + res.get = (path, ...filters) => get(response.data, path, ...filters); + return res; }; diff --git a/packages/bruno-js/tests/get.spec.js b/packages/bruno-js/tests/get.spec.js new file mode 100644 index 000000000..b5716ccd8 --- /dev/null +++ b/packages/bruno-js/tests/get.spec.js @@ -0,0 +1,46 @@ +const { filter } = require("lodash"); +const { get } = require("../src/get"); + +const data = { + customer: { + address: { + city: "bangalore" + }, + orders: [ + { + id: "order-1", + items: [ + { id: 1, amount: 10 }, + { id: 2, amount: 20 }, + ] + }, + { + id: "order-2", + items: [ + { id: 3, amount: 30, }, + { id: 4, amount: 40 } + ] + } + ] + }, +}; + +describe("get", () => { + it.each([ + ["customer.address.city", "bangalore"], + ["customer.orders.items.amount", [10, 20, 30, 40]], + ["customer.orders.items.amount[0]", 10], + ["..items.amount", [10, 20, 30, 40]], + ["..amount", [10, 20, 30, 40]], + ["..items.amount[0]", 10], + ["..items[0].amount", 10], + ["..items[?].amount", [40], (i) => i.amount > 30], // [?] filter + ["..id", ["order-1", 1, 2, "order-2", 3, 4]], // all ids + ["customer.orders.foo", undefined], + ["..customer.foo", undefined], + ["..address", [{ city: "bangalore" }]], // .. will return array + ["..address[0]", { city: "bangalore" }] + ])("%s should be %j %s", (expr, result, filter = undefined) => { + expect(get(data, expr, filter)).toEqual(result); + }); +}); From 209f30998e11ff53180b2624e140850cf87bd6ba Mon Sep 17 00:00:00 2001 From: Ajai Shankar Date: Sun, 12 Feb 2023 19:47:14 -0600 Subject: [PATCH 2/5] test: minor --- packages/bruno-js/src/get.js | 2 +- packages/bruno-js/tests/get.spec.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/bruno-js/src/get.js b/packages/bruno-js/src/get.js index 4a1e490f0..d58e24bb7 100644 --- a/packages/bruno-js/src/get.js +++ b/packages/bruno-js/src/get.js @@ -125,4 +125,4 @@ function get(source, path, ...filters) { module.exports = { get -}; \ No newline at end of file +}; diff --git a/packages/bruno-js/tests/get.spec.js b/packages/bruno-js/tests/get.spec.js index b5716ccd8..7e251ae70 100644 --- a/packages/bruno-js/tests/get.spec.js +++ b/packages/bruno-js/tests/get.spec.js @@ -39,7 +39,8 @@ describe("get", () => { ["customer.orders.foo", undefined], ["..customer.foo", undefined], ["..address", [{ city: "bangalore" }]], // .. will return array - ["..address[0]", { city: "bangalore" }] + ["..address[0]", { city: "bangalore" }], + ["..items..amount[?][0]", 40, amt => amt > 30] ])("%s should be %j %s", (expr, result, filter = undefined) => { expect(get(data, expr, filter)).toEqual(result); }); From 4fdfdaf2cbcbc4f71640f7fdd367c10be76aa5c3 Mon Sep 17 00:00:00 2001 From: Ajai Shankar Date: Sun, 19 Feb 2023 22:48:34 -0600 Subject: [PATCH 3/5] feat(query): bruno-query package --- package.json | 8 +- packages/bruno-query/.gitignore | 22 ++++ packages/bruno-query/jest.config.js | 5 + packages/bruno-query/package.json | 32 ++++++ packages/bruno-query/readme.md | 29 +++++ packages/bruno-query/rollup.config.js | 40 +++++++ packages/bruno-query/src/index.ts | 134 +++++++++++++++++++++++ packages/bruno-query/tests/index.spec.ts | 59 ++++++++++ packages/bruno-query/tsconfig.json | 23 ++++ 9 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-query/.gitignore create mode 100644 packages/bruno-query/jest.config.js create mode 100644 packages/bruno-query/package.json create mode 100644 packages/bruno-query/readme.md create mode 100644 packages/bruno-query/rollup.config.js create mode 100644 packages/bruno-query/src/index.ts create mode 100644 packages/bruno-query/tests/index.spec.ts create mode 100644 packages/bruno-query/tsconfig.json diff --git a/package.json b/package.json index 4eebca38c..f0455b986 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "packages/bruno-cli", "packages/bruno-tauri", "packages/bruno-schema", + "packages/bruno-query", "packages/bruno-js", "packages/bruno-lang", "packages/bruno-testbench", @@ -14,15 +15,18 @@ ], "devDependencies": { "@faker-js/faker": "^7.6.0", + "@jest/globals": "^29.2.0", "@playwright/test": "^1.27.1", "jest": "^29.2.0", - "randomstring": "^1.2.2" + "randomstring": "^1.2.2", + "ts-jest": "^29.0.5" }, "scripts": { "dev:web": "npm run dev --workspace=packages/bruno-app", "build:web": "npm run build --workspace=packages/bruno-app", "prettier:web": "npm run prettier --workspace=packages/bruno-app", "dev:electron": "npm run dev --workspace=packages/bruno-electron", + "build:bruno-query": "npm run build --workspace=packages/bruno-query", "build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs", "build:chrome-extension": "./scripts/build-chrome-extension.sh", "build:electron": "./scripts/build-electron.sh", @@ -32,4 +36,4 @@ "overrides": { "rollup": "3.2.5" } -} +} \ No newline at end of file diff --git a/packages/bruno-query/.gitignore b/packages/bruno-query/.gitignore new file mode 100644 index 000000000..f6eabff32 --- /dev/null +++ b/packages/bruno-query/.gitignore @@ -0,0 +1,22 @@ +# dependencies +node_modules +yarn.lock +pnpm-lock.yaml +package-lock.json +.pnp +.pnp.js + +# testing +coverage + +# production +dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/packages/bruno-query/jest.config.js b/packages/bruno-query/jest.config.js new file mode 100644 index 000000000..b413e106d --- /dev/null +++ b/packages/bruno-query/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/packages/bruno-query/package.json b/packages/bruno-query/package.json new file mode 100644 index 000000000..18f356095 --- /dev/null +++ b/packages/bruno-query/package.json @@ -0,0 +1,32 @@ +{ + "name": "@usebruno/query", + "version": "0.1.0", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "src", + "package.json" + ], + "scripts": { + "clean": "rimraf dist", + "test": "jest", + "prebuild": "npm run clean", + "build": "rollup -c", + "prepack": "npm run test && npm run build" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^9.0.2", + "rollup": "3.2.5", + "rollup-plugin-dts": "^5.0.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-terser": "^7.0.2", + "typescript": "^4.8.4" + }, + "overrides": { + "rollup": "3.2.5" + } +} \ No newline at end of file diff --git a/packages/bruno-query/readme.md b/packages/bruno-query/readme.md new file mode 100644 index 000000000..aeb709c65 --- /dev/null +++ b/packages/bruno-query/readme.md @@ -0,0 +1,29 @@ +# bruno-query + +Bruno query with deep navigation, filter and map support + +Easy array navigation +```js +get(data, 'customer.orders.items.amount') +``` +Deep navigation .. double dots +```js +get(data, '..items.amount') +``` +Array indexing +```js +get(data, '..items[0].amount') +``` +Array filtering [?] with corresponding filter function +```js +get(data, '..items[?].amount', i => i.amount > 20) +``` +Array mapping [?] with corresponding mapper function +```js +get(data, '..items[?].amount', i => i.amount + 10) +``` + +### Publish to Npm Registry +```bash +npm publish --access=public +``` \ No newline at end of file diff --git a/packages/bruno-query/rollup.config.js b/packages/bruno-query/rollup.config.js new file mode 100644 index 000000000..67dc6b113 --- /dev/null +++ b/packages/bruno-query/rollup.config.js @@ -0,0 +1,40 @@ +const { nodeResolve } = require("@rollup/plugin-node-resolve"); +const commonjs = require("@rollup/plugin-commonjs"); +const typescript = require("@rollup/plugin-typescript"); +const dts = require("rollup-plugin-dts"); +const { terser } = require("rollup-plugin-terser"); +const peerDepsExternal = require('rollup-plugin-peer-deps-external'); + +const packageJson = require("./package.json"); + +module.exports = [ + { + input: "src/index.ts", + output: [ + { + file: packageJson.main, + format: "cjs", + sourcemap: true, + }, + { + file: packageJson.module, + format: "esm", + sourcemap: true, + }, + ], + plugins: [ + peerDepsExternal(), + nodeResolve({ + extensions: ['.css'] + }), + commonjs(), + typescript({ tsconfig: "./tsconfig.json" }), + terser() + ] + }, + { + input: "dist/esm/index.d.ts", + output: [{ file: "dist/index.d.ts", format: "esm" }], + plugins: [dts.default()], + } +]; \ No newline at end of file diff --git a/packages/bruno-query/src/index.ts b/packages/bruno-query/src/index.ts new file mode 100644 index 000000000..5c29c510e --- /dev/null +++ b/packages/bruno-query/src/index.ts @@ -0,0 +1,134 @@ +/** + * If value is an array returns the deeply flattened array, otherwise value + */ +function normalize(value: any) { + if (!Array.isArray(value)) return value; + + const values = [] as any[]; + + value.forEach(item => { + const value = normalize(item); + if (value != null) { + values.push(...Array.isArray(value) ? value : [value]); + } + }); + + return values.length ? values : undefined; +} + +/** + * Gets value of a prop from source. + * + * If source is an array get value for each item. + * + * If deep is true then recursively gets values for prop in nested objects. + * + * Once a value if found will not recurese further into that value. + */ +function getValue(source: any, prop: string, deep = false): any { + if (typeof source !== 'object') return; + + let value; + + if (Array.isArray(source)) { + value = source.map(item => getValue(item, prop, deep)); + } else { + value = source[prop]; + if (deep) { + value = [value]; + for (const [key, item] of Object.entries(source)) { + if (key !== prop && typeof item === 'object') { + value.push(getValue(source[key], prop, deep)); + } + } + } + } + + return normalize(value); +} + +type PredicateOrMapper = (obj: any) => any; + +/** + * Apply filter on source array or object + * + * If the filter returns a non boolean non null value it is treated as a mapped value + */ +function filterOrMap(source: any, fun: PredicateOrMapper) { + const isArray = Array.isArray(source); + const list = isArray ? source : [source]; + const result = [] as any[]; + for (const item of list) { + if (item == null) continue; + const value = fun(item); + if (value === true) { + result.push(item); // predicate + } else if (value != null && value !== false) { + result.push(value); // mapper + } + } + return isArray ? result : result[0]; +} + +/** + * Getter with deep navigation, filter and map support + * + * 1. Easy array navigation + * ```js + * get(data, 'customer.orders.items.amount') + * ``` + * 2. Deep navigation .. double dots + * ```js + * get(data, '..items.amount') + * ``` + * 3. Array indexing + * ```js + * get(data, '..items[0].amount') + * ``` + * 4. Array filtering [?] with corresponding filter function + * ```js + * get(data, '..items[?].amount', i => i.amount > 20) + * ``` + * 5. Array mapping [?] with corresponding mapper function + * ```js + * get(data, '..items[?].amount', i => i.amount + 10) + * ``` + */ +export function get(source: any, path: string, ...fns: PredicateOrMapper[]) { + const paths = path + .replace(/\s+/g, '') + .split(/(\.{1,2}|\[\?\]|\[\d+\])/g) // ["..", "items", "[?]", ".", "amount", "[0]" ] + .filter(s => s.length > 0) + .map(str => { + str = str.replace(/\[|\]/g, ''); + const index = parseInt(str); + return isNaN(index) ? str : index; + }); + + let index = 0, lookbehind = '' as string | number, funIndex = 0; + + while (source != null && index < paths.length) { + const token = paths[index++]; + + switch (true) { + case token === "..": + case token === ".": + break; + case token === "?": + const fun = fns[funIndex++]; + if (fun == null) + throw new Error(`missing function for ${lookbehind}`); + source = filterOrMap(source, fun); + break; + case typeof token === 'number': + source = source[token]; + break; + default: + source = getValue(source, token as string, lookbehind === ".."); + } + + lookbehind = token; + } + + return source; +} \ No newline at end of file diff --git a/packages/bruno-query/tests/index.spec.ts b/packages/bruno-query/tests/index.spec.ts new file mode 100644 index 000000000..10ef027a8 --- /dev/null +++ b/packages/bruno-query/tests/index.spec.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from '@jest/globals'; + +import { get } from '../src/index'; + +const data = { + customer: { + address: { + city: "bangalore" + }, + orders: [ + { + id: "order-1", + items: [ + { id: 1, amount: 10 }, + { id: 2, amount: 20 }, + ] + }, + { + id: "order-2", + items: [ + { id: 3, amount: 30, }, + { id: 4, amount: 40 } + ] + } + ] + }, +}; + +describe("get", () => { + it.each([ + ["customer.address.city", "bangalore"], + ["customer.orders.items.amount", [10, 20, 30, 40]], + ["customer.orders.items.amount[0]", 10], + ["..items.amount", [10, 20, 30, 40]], + ["..amount", [10, 20, 30, 40]], + ["..items.amount[0]", 10], + ["..items[0].amount", 10], + ["..items[5].amount", undefined], // invalid index + ["..id", ["order-1", 1, 2, "order-2", 3, 4]], // all ids + ["customer.orders.foo", undefined], + ["..customer.foo", undefined], + ["..address", [{ city: "bangalore" }]], // .. will return array + ["..address[0]", { city: "bangalore" }], + ])("%s should be %j", (expr, result) => { + expect(get(data, expr)).toEqual(result); + }); + + // filter and map + it.each([ + ["..items[?].amount", [40], (i: any) => i.amount > 30], // [?] filter + ["..items..amount[?][0]", 40, (amt: number) => amt > 30], + ["..items..amount[0][?]", undefined, (amt: number) => amt > 30], // filter on single value + ["..items..amount[?]", [11, 21, 31, 41], (amt: number) => amt + 1], // [?] mapper + ["..items..amount[0][?]", 11, (amt: number) => amt + 1], // [?] map on single value + ])("%s should be %j %s", (expr, result, filter) => { + expect(get(data, expr, filter)).toEqual(result); + }); + +}); diff --git a/packages/bruno-query/tsconfig.json b/packages/bruno-query/tsconfig.json new file mode 100644 index 000000000..96998f7a1 --- /dev/null +++ b/packages/bruno-query/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES6", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "jsx": "react", + "module": "ESNext", + "declaration": true, + "declarationDir": "types", + "sourceMap": true, + "outDir": "dist", + "moduleResolution": "node", + "emitDeclarationOnly": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": [ + "dist", + "node_modules", + "tests" + ], +} \ No newline at end of file From 2dfc9729304f40cae97838a370c0eedcc54f62f7 Mon Sep 17 00:00:00 2001 From: Ajai Shankar Date: Sun, 19 Feb 2023 23:35:49 -0600 Subject: [PATCH 4/5] feat: res default to bruno query --- packages/bruno-js/package.json | 5 +- packages/bruno-js/src/get.js | 128 -------------------------- packages/bruno-js/src/utils.js | 31 ++----- packages/bruno-js/tests/get.spec.js | 47 ---------- packages/bruno-js/tests/utils.spec.js | 27 +++++- 5 files changed, 36 insertions(+), 202 deletions(-) delete mode 100644 packages/bruno-js/src/get.js delete mode 100644 packages/bruno-js/tests/get.spec.js diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json index 2a907eee6..88680c26d 100644 --- a/packages/bruno-js/package.json +++ b/packages/bruno-js/package.json @@ -21,6 +21,7 @@ "lodash": "^4.17.21", "moment": "^2.29.4", "nanoid": "3.3.4", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "@usebruno/query": "0.1.0" } -} +} \ No newline at end of file diff --git a/packages/bruno-js/src/get.js b/packages/bruno-js/src/get.js deleted file mode 100644 index d58e24bb7..000000000 --- a/packages/bruno-js/src/get.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Gets property values for all items in source array - */ -const arrayGet = (source, prop) => { - if (!Array.isArray(source)) return []; - - const results = []; - - source.forEach(item => { - const value = item[prop]; - if (value != null) { - results.push(...Array.isArray(value) ? value : [value]); - } - }); - - return results; -}; - -/** - * Recursively collects property values into results - */ -const deepGet = (source, prop, results) => { - if (source == null || typeof source !== 'object') return; - - if (Array.isArray(source)) { - source.forEach(item => deepGet(item, prop, results)); - } else { - for (const key in source) { - if (key === prop) { - const value = source[prop]; - results.push(...Array.isArray(value) ? value : [value]); - } else { - deepGet(source[key], prop, results); - } - } - } -}; - -/** - * Gets property value(s) from source - */ -const baseGet = (source, prop, deep = false) => { - if (source == null || typeof source !== 'object') return; - - if (!deep) { - return Array.isArray(source) ? arrayGet(source, prop) : source[prop]; - } else { - const results = []; - deepGet(source, prop, results); - return results.filter(value => value != null); - } -}; - -/** - * Apply filter on source array or object - */ -const applyFilter = (source, predicate, single = false) => { - const list = Array.isArray(source) ? source : [source]; - const result = list.filter(predicate); - return single ? result[0] : result; -}; - -/** - * Supercharged getter with deep navigation and filter support - * - * 1. Easy array navigation - * ```js - * get(data, 'customer.orders.items.amount') - * ``` - * 2. Deep navigation .. double dots - * ```js - * get(data, '..items.amount') - * ``` - * 3. Array indexing - * ```js - * get(data, '..items[0].amount') - * ``` - * 4. Array filtering [?] with corresponding js filter - * ```js - * get(data, '..items[?].amount', i => i.amount > 20) - * ``` - */ -function get(source, path, ...filters) { - const paths = path - .replace(/\s+/g, '') - .split(/(\.{1,2}|\[\?\]|\[\d+\])/g) // ["..", "items", "[?]", ".", "amount", "[0]" ] - .filter(s => s.length > 0) - .map(str => { - str = str.replace(/\[|\]/g, ''); - const index = parseInt(str); - return isNaN(index) ? str : index; - }); - - let index = 0, lookbehind = '', filterIndex = 0; - - while (source != null && index < paths.length) { - const token = paths[index++]; - - switch (true) { - case token === "..": - case token === ".": - break; - case token === "?": - const filter = filters[filterIndex++]; - if (filter == null) - throw new Error(`missing filter for ${lookbehind}`); - const single = !Array.isArray(source); - source = applyFilter(source, filter, single); - break; - case typeof token === 'number': - source = source[token]; - break; - default: - source = baseGet(source, token, lookbehind === ".."); - if (Array.isArray(source) && !source.length) { - source = undefined; - } - } - - lookbehind = token; - } - - return source; -} - -module.exports = { - get -}; diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 151bb6014..262926905 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -1,5 +1,5 @@ const jsonQuery = require('json-query'); -const { get } = require("./get"); +const { get } = require("@usebruno/query"); const JS_KEYWORDS = ` break case catch class const continue debugger default delete do @@ -61,9 +61,8 @@ const evaluateJsExpression = (expression, context) => { }; const createResponseParser = (response = {}) => { - const res = (expr) => { - const output = jsonQuery(expr, { data: response.data }); - return output ? output.value : null; + const res = (expr, ...fns) => { + return get(response.data, expr, ...fns); }; res.status = response.status; @@ -71,26 +70,10 @@ const createResponseParser = (response = {}) => { res.headers = response.headers; res.body = response.data; - /** - * Get supports deep object navigation and filtering - * 1. Easy array navigation - * ```js - * res.get('customer.orders.items.amount') - * ``` - * 2. Deep navigation .. double dots - * ```js - * res.get('..items.amount') - * ``` - * 3. Array indexing - * ```js - * res.get('..items[0].amount') - * ``` - * 4. Array filtering [?] with corresponding js filter - * ```js - * res.get('..items[?].amount', i => i.amount > 20) - * ``` - */ - res.get = (path, ...filters) => get(response.data, path, ...filters); + res.jq = (expr) => { + const output = jsonQuery(expr, { data: response.data }); + return output ? output.value : null; + }; return res; }; diff --git a/packages/bruno-js/tests/get.spec.js b/packages/bruno-js/tests/get.spec.js deleted file mode 100644 index 7e251ae70..000000000 --- a/packages/bruno-js/tests/get.spec.js +++ /dev/null @@ -1,47 +0,0 @@ -const { filter } = require("lodash"); -const { get } = require("../src/get"); - -const data = { - customer: { - address: { - city: "bangalore" - }, - orders: [ - { - id: "order-1", - items: [ - { id: 1, amount: 10 }, - { id: 2, amount: 20 }, - ] - }, - { - id: "order-2", - items: [ - { id: 3, amount: 30, }, - { id: 4, amount: 40 } - ] - } - ] - }, -}; - -describe("get", () => { - it.each([ - ["customer.address.city", "bangalore"], - ["customer.orders.items.amount", [10, 20, 30, 40]], - ["customer.orders.items.amount[0]", 10], - ["..items.amount", [10, 20, 30, 40]], - ["..amount", [10, 20, 30, 40]], - ["..items.amount[0]", 10], - ["..items[0].amount", 10], - ["..items[?].amount", [40], (i) => i.amount > 30], // [?] filter - ["..id", ["order-1", 1, 2, "order-2", 3, 4]], // all ids - ["customer.orders.foo", undefined], - ["..customer.foo", undefined], - ["..address", [{ city: "bangalore" }]], // .. will return array - ["..address[0]", { city: "bangalore" }], - ["..items..amount[?][0]", 40, amt => amt > 30] - ])("%s should be %j %s", (expr, result, filter = undefined) => { - expect(get(data, expr, filter)).toEqual(result); - }); -}); diff --git a/packages/bruno-js/tests/utils.spec.js b/packages/bruno-js/tests/utils.spec.js index 6d5212407..ef8973542 100644 --- a/packages/bruno-js/tests/utils.spec.js +++ b/packages/bruno-js/tests/utils.spec.js @@ -1,4 +1,5 @@ -const { evaluateJsExpression, internalExpressionCache: cache } = require("../src/utils"); +const { describe, it, expect } = require("@jest/globals"); +const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser } = require("../src/utils"); describe("utils", () => { describe("expression evaluation", () => { @@ -112,4 +113,28 @@ describe("utils", () => { expect(result).toBe(startTime); }); }); + + describe("response parser", () => { + const res = createResponseParser({ + status: 200, + data: { + order: { + items: [ + { id: 1, amount: 10 }, + { id: 2, amount: 20 } + ] + } + } + }); + + it("should default to bruno query", () => { + const value = res("..items[?].amount[0]", i => i.amount > 10); + expect(value).toBe(20); + }); + + it("should allow json-query", () => { + const value = res.jq("order.items[amount > 10].amount"); + expect(value).toBe(20); + }); + }); }); From c5d43cc9e6f5271d290bd688671167842739fa05 Mon Sep 17 00:00:00 2001 From: Ajai Shankar Date: Sun, 19 Feb 2023 23:51:47 -0600 Subject: [PATCH 5/5] chore: add bruno-query test/build to github workflows --- .github/workflows/unit-tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index e2ec33a9d..49b558f66 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -15,6 +15,10 @@ jobs: node-version: 16 - name: Install dependencies run: npm i --legacy-peer-deps + - name: Test Package bruno-query + run: npm run test --workspace=packages/bruno-query + - name: Build Package bruno-query + run: npm run build --workspace=packages/bruno-query - name: Test Package bruno-lang run: npm run test --workspace=packages/bruno-lang - name: Test Package bruno-schema