forked from extern/bruno
feat(query): bruno-query package
This commit is contained in:
parent
209f30998e
commit
4fdfdaf2cb
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
22
packages/bruno-query/.gitignore
vendored
Normal file
22
packages/bruno-query/.gitignore
vendored
Normal file
@ -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*
|
5
packages/bruno-query/jest.config.js
Normal file
5
packages/bruno-query/jest.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
32
packages/bruno-query/package.json
Normal file
32
packages/bruno-query/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
29
packages/bruno-query/readme.md
Normal file
29
packages/bruno-query/readme.md
Normal file
@ -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
|
||||
```
|
40
packages/bruno-query/rollup.config.js
Normal file
40
packages/bruno-query/rollup.config.js
Normal file
@ -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()],
|
||||
}
|
||||
];
|
134
packages/bruno-query/src/index.ts
Normal file
134
packages/bruno-query/src/index.ts
Normal file
@ -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;
|
||||
}
|
59
packages/bruno-query/tests/index.spec.ts
Normal file
59
packages/bruno-query/tests/index.spec.ts
Normal file
@ -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);
|
||||
});
|
||||
|
||||
});
|
23
packages/bruno-query/tsconfig.json
Normal file
23
packages/bruno-query/tsconfig.json
Normal file
@ -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"
|
||||
],
|
||||
}
|
Loading…
Reference in New Issue
Block a user