├── .editorconfig ├── .eslintrc.yml ├── .github └── workflows │ ├── main.yaml │ └── publish.yaml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── Makefile ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── _config.yml ├── demo ├── css │ └── style.css ├── index.html └── js │ └── interaction.js ├── package-lock.json ├── package.json └── src ├── browser └── index.js ├── index.js ├── index.test.js ├── parsers ├── simple.js ├── simple.test.js ├── util.js ├── util.spec.js ├── xpath.js └── xpath.test.js ├── picker.js └── picker.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,yaml,json}] 2 | indent_style = space 3 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es6: true 4 | jest: true 5 | node: true 6 | extends: 'eslint:recommended' 7 | parserOptions: 8 | sourceType: module 9 | rules: 10 | indent: 11 | - error 12 | - 2 13 | linebreak-style: 14 | - error 15 | - unix 16 | quotes: 17 | - error 18 | - single 19 | semi: 20 | - error 21 | - never 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push] 3 | 4 | jobs: 5 | lint: 6 | name: Lint/Test 7 | # This job runs on Linux 8 | runs-on: ubuntu-18.04 9 | strategy: 10 | matrix: 11 | node: [ '8', '9', '10', '11', '12' ] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@master 15 | - name: Setup node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - name: Install DEV dependencies 20 | run: npm i 21 | - name: Tests 22 | run: make test 23 | - name: Lint 24 | run: make lint 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - v*.*.* 6 | branches: 7 | - master 8 | 9 | jobs: 10 | lint: 11 | name: Publish 12 | runs-on: ubuntu-18.04 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@master 16 | - name: Setup node 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | - name: Publish package 21 | env: 22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | run: | 24 | npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN 25 | npm publish 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled dist files 33 | dist/ 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | yarn.lock 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # This file is equal to .gitignore BUT without "dist" dir. 2 | # you can keep it igual to .gitignore except with "dist" dir. 3 | 4 | # Demo page 5 | demo/ 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Typescript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | yarn.lock 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | *Add here new changes respecting [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) instructions.* 11 | 12 | ## [v1.0.0] - 2019-10-06 13 | 14 | ### Changed 15 | - Change to use Github actions. 16 | - Update dev dependencies (`eslint` and `jest`). 17 | - Only branch master can publish new tags. 18 | 19 | ### Removed 20 | - Browser support (webpack, babel, etc...). 21 | 22 | ## [v0.4.1] - 2018-10-03 23 | 24 | ### Changed 25 | - Improve README documentation. 26 | 27 | ## [v0.4.0] - 2018-07-12 28 | 29 | ### Added 30 | - Changelog. 31 | - Demo version of deepicker 32 | 33 | ### Security 34 | - Updated package-lock removing `hoek` dependency vulnerability. 35 | 36 | ## [v0.3.8] - 2018-02-19 37 | 38 | ### Added 39 | - `.npmignore` to publish `dist` dir with minified versions. 40 | 41 | ## [v0.3.7] - 2018-02-16 42 | 43 | ### Added 44 | - Browser version. (thanks for amazing contribution of @guicheffer!) 45 | - Add yarn. (again, thanks @guicheffer) 46 | - Add a precommit hook to run lint and tests. (and again, thanks @guicheffer) 47 | - Add an issue template. (and again, thanks @guicheffer) 48 | - Add a pull request template. (and again, thanks @guicheffer) 49 | - Add a how to contribute file. (and again, thanks @guicheffer) 50 | 51 | ## [v0.3.6] - 2018-02-09 52 | 53 | ### Changed 54 | - Fix `pickStatic` method to handle `null` or `undefined`. 55 | 56 | ## [v0.3.5] - 2018-02-08 57 | 58 | ### Changed 59 | - Pickers (static and normal) now use common `include` method. 60 | - Include method not receive anymore include/exclude trees. 61 | - Fix `include` method when `*` has a subtree. 62 | 63 | ## [v0.3.4] - 2018-02-07 64 | 65 | ### Changed 66 | - `include` method now receives include/exclude trees. 67 | - Fix `include` method when include keys is filled. 68 | 69 | ## [v0.3.3] - 2018-02-07 70 | 71 | ### Added 72 | - Github pages files 73 | 74 | ### Changed 75 | - Fix `include` method when `*` has subtree. 76 | 77 | ## [v0.3.2] - 2018-02-07 78 | 79 | ### Changed 80 | - Fix `toContext` method when handle `*` wildcard. 81 | 82 | ## [v0.3.1] - 2018-02-07 83 | 84 | ### Changed 85 | - Fix linter issues. 86 | 87 | ## [v0.3.0] - 2018-02-07 88 | 89 | ### Added 90 | - `toContext` function to move picker to a new context. 91 | 92 | ### Changed 93 | - Fix `include` when handling `*` wild card. 94 | 95 | ## [v0.2.1] - 2018-02-06 96 | 97 | ### Changed 98 | - Change `pickStatic` to handle arrays. 99 | - Update `npm install` in circle CI. 100 | 101 | ## [v0.2.0] - 2018-02-06 102 | 103 | ### Added 104 | - `pickStatic` function to handle simple js objects. 105 | 106 | ## [v0.1.2] - 2018-02-06 107 | 108 | ### Added 109 | - Handle `*` wildcard in simple parser for include/exclude strings. 110 | - Function to merge trees in include/exclude strings (so, removing deepmerge lib). 111 | 112 | ### Changed 113 | - Change xpath parser of include/exclude to use new merge function. 114 | - Handle better `*` when merging with other trees (include/exclude parser). 115 | 116 | ### Removed 117 | - Lib deepmerge dependecy 118 | 119 | ## [v0.1.1] - 2018-02-05 120 | 121 | ### Changed 122 | - Fix linter issues 123 | 124 | ## [v0.1.0] - 2018-02-05 125 | 126 | ### Added 127 | - Handle `*` wildcard in xpath parser of include/exclude. 128 | - Tests for simple parser function with `*` wildcard. 129 | - Tests for exclude with arrays structure. 130 | 131 | ### Changed 132 | - Improve README. 133 | 134 | ## [v0.0.3] - 2018-02-05 135 | 136 | ### Changed 137 | - Fix promises array resolution. 138 | 139 | ## [v0.0.2] - 2018-02-05 140 | 141 | ### Added 142 | - First valid lib version. 143 | - Npm deployment on circle ci. 144 | - Method `include` to be used to test if some key must be included. 145 | - Linter 146 | - Readme 147 | - Parser `simpleParser` to parse simple include/excludes strings. 148 | - CircleCi 149 | - Parser `xpathParser` to parse "xPath like" strings for include/exclude. 150 | - Add Editor config file. 151 | 152 | ### Changed 153 | - Change to pass picker instance to nested function. 154 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Guidance on how to contribute 2 | 3 | **First of all! Thank you for your contribution to this project, it's very welcome! Let's make it cool and useful for everyone!** 4 | 5 | There are two primary ways to help: 6 | - Using the issue tracker, and 7 | - Changing the code-base. 8 | 9 | ## Using the issue tracker 10 | 11 | Use the issue tracker to suggest feature requests, report bugs, and ask questions. 12 | This is also a great way to connect with the developers of the project as well 13 | as others who are interested in this solution. 14 | 15 | Use the issue tracker to find ways to contribute. Find a bug or a feature, mention in 16 | the issue that you will take on that effort, then follow the _Changing the code-base** 17 | guidance below. 18 | 19 | 20 | ## Changing the code-base 21 | 22 | Generally speaking, you should fork this repository, make changes in your 23 | own fork, and then submit a pull-request. All new code should have associated unit tests that validate implemented features and the presence or lack of defects. 24 | 25 | Additionally, the code should follow any stylistic and architectural guidelines 26 | prescribed by the project. In the absence of such guidelines, mimic the styles 27 | and patterns in the existing code-base. 28 | 29 | ## Updating changelog 30 | 31 | When you open a pull-request it's import to add your changes to **Unreleased** section in our CHANGELOG file respecting `Added`, `Changed` and `Removed` subsections (take a look [here](http://keepachangelog.com/en/1.0.0/) for more information). It helps us to keep on track (and other people) about what we'll publish in our next release and what we have published in previous releases. 32 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Enhancement (if applicable) 4 | 5 | 6 | 7 | ## Expected Behavior (if applicable) 8 | 9 | 10 | 11 | ## Current Behavior (if applicable) 12 | 13 | 14 | 15 | ## Possible Solution 16 | 17 | 18 | 19 | ## Steps to Reproduce (for bugs) (if appropriate) 20 | 21 | 22 | - 1; 23 | - 2; 24 | - 3; 25 | - 4. 26 | 27 | ## Context 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alisson R. Perez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo 3 | @echo "✍🏽 Please use 'make ' where is one of the commands below:" 4 | @echo 5 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e "s/\\$$//" | sed -e "s/##//" 6 | @echo 7 | 8 | check: lint test 9 | 10 | lint: ## lints javascript lines 11 | npm run lint 12 | 13 | test: ## runs unit tests 14 | npm run test 15 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation, Consideration and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ## Screenshots (if appropriate): 16 | 17 | ## Types of changes 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 22 | 23 | **Checklist**: 24 | 25 | 26 | - [ ] My code follows the code style of this project. 27 | - [ ] My change requires a change to the documentation. 28 | - [ ] I have updated the documentation accordingly. 29 | - [ ] I have added changes to changelog. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/alissonperez/deepicker/Build/badge.svg) ![](https://github.com/alissonperez/deepicker/Version%20tagged/badge.svg) 2 | 3 | Deepicker 4 | ============= 5 | 6 | A tiny library inspired in GraphQL but much **more simple without buracratic things and large libraries** for clients and servers! 7 | 8 | Features: 9 | 10 | - Simple implementation. 11 | - Increases performance processing only what client asks for. 12 | - Non-blocking processing, it handles promisses in parallel very well. 13 | - Simple usage by clients. Just receive an `include` and `exclude` querystring and send them to Deepicker. 14 | - No worries about dependencies, Deepicker is pure JS implementation. 15 | 16 | ### Installation 17 | 18 | Node support: **8+** 19 | 20 | ```bash 21 | $ npm i --save deepicker 22 | ``` 23 | 24 | or, if you prefer `yarn`: 25 | 26 | ```bash 27 | $ yarn add deepicker 28 | ``` 29 | 30 | ### Usage 31 | 32 | #### Quickstart example 33 | 34 | Let's see a very simple example to warmup: 35 | 36 | ```javascript 37 | const deepicker = require('deepicker') 38 | 39 | // My object with a simple structure (for now!) 40 | const myObject = { 41 | title: 'Star Wars: Episode V - The Empire Strikes Back', 42 | episodeNumber: '5', 43 | releaseYear: 1980, 44 | characters: [ 45 | { name: 'Luke Skywalker', actor: 'Mark Hamill' }, 46 | { name: 'Han Solo', actor: 'Harrison Ford' }, 47 | { name: 'Princess Leia Organa', actor: 'Carrie Fischer' }, 48 | { name: 'Darth Vader', actor: 'James Earl Jones' }, 49 | // ... 50 | ], 51 | description: 'Fleeing the evil Galactic Empire, the Rebels abandon...', 52 | } 53 | 54 | // Include/exclude string, you can receive then in a request querystring, for example 55 | const include = 'title,description,characters' 56 | const exclude = 'characters.actor' 57 | 58 | // Create our picker object 59 | const picker = deepicker.simplePicker(include, exclude) 60 | 61 | // Let's process our object, picker will pick only especified fields deeply. 62 | console.log(picker.pick(myObject)) 63 | ``` 64 | 65 | As a result: 66 | 67 | ```json 68 | { 69 | "title":"Star Wars: Episode V - The Empire Strikes Back", 70 | "description":"Fleeing the evil Galactic Empire, the Rebels abandon...", 71 | "characters":[ 72 | { "name":"Luke Skywalker" }, 73 | { "name":"Han Solo" }, 74 | { "name":"Princess Leia Organa" }, 75 | { "name":"Darth Vader" } 76 | ] 77 | } 78 | ``` 79 | 80 | 81 | #### Using nested functions 82 | 83 | But we know, unfortunatelly, real life "usually" is not so simple. Thinking about it, Deepicker is able to handle a lot of data types inside our objects, like arrays, promises, functions... etc. And with this last data type is where Deepicker really shines and show its power! =) 84 | 85 | Let's see a more complex example using functions: 86 | 87 | ```javascript 88 | const deepicker = require('deepicker') 89 | 90 | const myObject = { 91 | title: 'Star Wars: Episode V - The Empire Strikes Back', 92 | description: 'Fleeing the evil Galactic Empire, the Rebels abandon...', 93 | releaseYear: 1980, 94 | 95 | // Here, using a function to computate our 'nextMovie' key content (pay attention to "picker" arg) 96 | nextMovie: function(picker) { 97 | const movie = { 98 | title: 'Star Wars: Episode IV - A New Hope)', 99 | description: 'The galaxy is in the midst of a civil war. Spies for the Rebel Alliance have stolen plans...', 100 | releaseYear: 1977 101 | } 102 | 103 | // Pay attention here, picker instance is with 'nextMovie' context, 104 | // so, when we call "pick" method it knows exactly what needs to cut out 105 | // or not. 106 | return picker.pick(movie) 107 | }, 108 | 109 | previousMovie: function(picker) { 110 | const movie = { 111 | title: 'Star Wars: Episode VI - Return of the Jedi', 112 | description: 'The Galactic Empire, under the direction of the ruthless Emperor...', 113 | releaseYear: 1983 114 | } 115 | 116 | return picker.pick(movie) 117 | } 118 | } 119 | 120 | // Now, we add "nextMovie" in our include fields and exclude "releaseYear" from "nextMovie". 121 | const include = 'title,description,nextMovie' 122 | const exclude = 'nextMovie.releaseYear' 123 | 124 | // Create our picker object 125 | const picker = deepicker.simplePicker(include, exclude) 126 | 127 | // Let's pick! 128 | console.log(picker.pick(myObject)) 129 | ``` 130 | 131 | And we have the following as result: 132 | 133 | ```json 134 | { 135 | "title":"Star Wars: Episode V - The Empire Strikes Back", 136 | "description":"Fleeing the evil Galactic Empire, the Rebels abandon...", 137 | "nextMovie":{ 138 | "title":"Star Wars: Episode IV - A New Hope)", 139 | "description":"The galaxy is in the midst of a civil war. Spies for the Rebel Alliance have stolen plans..." 140 | } 141 | } 142 | ``` 143 | 144 | Ok, what happened?! 145 | 146 | First of all, we didn't ask for "previousMovie", so deepicker don't evaluate this function. In this example this don't affect performance, but thinking about an operation like access to a database to get something or call some API this can increase performance significantly. The main gain here is **we process only what client asks for**. 147 | 148 | Other thing, pay attention to `picker` arg received in our functions. This picker instance is with correct context, in this example, with `releaseYear` in exclude option. This is very important to pick content inside these functions and, as you can imagine, we can the same `pick` operation nested: 149 | 150 | 151 | ```javascript 152 | nextMovie: function(picker) { 153 | const movie = { 154 | title: 'Star Wars: Episode IV - A New Hope)', 155 | description: 'The galaxy is in the midst of a civil war. Spies for the Rebel Alliance have stolen plans...', 156 | releaseYear: 1977, 157 | 158 | // Using a function to compute otherInfo about Episode IV 159 | otherInfo: function(picker) { 160 | // Perform somethig here to get movie info and return then 161 | 162 | return picker.pick({ 163 | directedBy: "George Lucas", 164 | producedBy: "Gary Kurtz", 165 | writtenBy: "George Lucas" 166 | }) 167 | } 168 | } 169 | 170 | return picker.pick(movie) 171 | }, 172 | ``` 173 | 174 | In this example, we can use `nextMovie.otherInfo.directedBy` in our `include` option to get only "George Lucas" name and exclude the other info. Or, `otherInfo` function even is called with `include` only with `nextMovie.releaseYear`. 175 | 176 | #### Using promises 177 | 178 | All we know that using JS is use asyncronous code, a library in node with no support for that is useless. So, Deepicker has a very good support to use Promises, lets take a look in an example: 179 | 180 | ```javascript 181 | const deepicker = require('deepicker') 182 | 183 | const myObject = { 184 | // First, we can have promises direct associated with our keys 185 | // Deepicker will "resolve" it for us 186 | title: Promise.resolve('Star Wars: Episode V - The Empire Strikes Back'), 187 | 188 | description: 'Fleeing the evil Galactic Empire, the Rebels abandon...', 189 | releaseYear: 1980, 190 | 191 | // Our function to calculate next movie 192 | nextMovie: function(picker) { 193 | // Let's think we need to get next movie from an API, so we'll need an asyncronous request 194 | return new Promise((resolve, reject) => { 195 | // ... calling some API here 196 | 197 | const movie = { 198 | title: 'Star Wars: Episode IV - A New Hope)', 199 | description: 'The galaxy is in the midst of a civil war. Spies for the Rebel Alliance have stolen plans...', 200 | releaseYear: 1977 201 | } 202 | 203 | resolve(picker.pick(movie)) 204 | }) 205 | }, 206 | 207 | // Our function to calculate previous movie 208 | previousMovie: function(picker) { 209 | // Let's think we need to get next movie from an API, so we'll need an asyncronous request 210 | return new Promise((resolve, reject) => { 211 | // ... calling some API here 212 | 213 | const movie = { 214 | title: 'Star Wars: Episode VI - Return of the Jedi', 215 | description: 'The Galactic Empire, under the direction of the ruthless Emperor...', 216 | releaseYear: 1983 217 | } 218 | 219 | resolve(picker.pick(movie)) 220 | }) 221 | } 222 | } 223 | 224 | // Our include / exclude as usually 225 | const include = 'title,description,nextMovie' 226 | const exclude = 'nextMovie.releaseYear' 227 | 228 | const picker = deepicker.simplePicker(include, exclude) 229 | 230 | // To handle with promises in our object, we need to use 'pickPromise' method 231 | picker.pickPromise(myObject).then(result => { 232 | console.log(result) 233 | }) 234 | ``` 235 | 236 | As result: 237 | 238 | ``` 239 | { 240 | "title":"Star Wars: Episode V - The Empire Strikes Back", 241 | "description":"Fleeing the evil Galactic Empire, the Rebels abandon...", 242 | "nextMovie":{ 243 | "title":"Star Wars: Episode IV - A New Hope)", 244 | "description":"The galaxy is in the midst of a civil war. Spies for the Rebel Alliance have stolen plans..." 245 | } 246 | } 247 | ``` 248 | 249 | ### Reference 250 | 251 | #### Constructor 252 | 253 | Building our picker is the first step to use it. Each constructors available uses one kind of include and exclude string. 254 | 255 | ##### deepicker.simplePicker([includeStr], [excludeStr]) 256 | 257 | This option handles simple include and exclude string, e.g.: 258 | 259 | ```javascript 260 | const include = 'title,description,nextMovie' 261 | const exclude = 'nextMovie.releaseYear' 262 | 263 | const picker = deepicker.simplePicker(include, exclude) 264 | ``` 265 | 266 | As you see, our `include` and `exclude` vars are simple strings with fields separated by `,` and using `.` to going into objects. Always consider root element to build this string. 267 | 268 | ##### deepicker.xpathPicker([includeStr], [excludeStr]) 269 | 270 | This option handle include and exclude string with a "xpath like" format, e.g.: 271 | 272 | ```javascript 273 | const include = 'nextMovie/releaseYear,characters(name,actor)' 274 | const exclude = 'previousMovie/characters/name' 275 | 276 | const picker = deepicker.xpathPicker(include, exclude) 277 | ``` 278 | 279 | As you see, it uses `/` to going deep into fields object and `,` to separate options. The great advantage to use this way is to save "bytes" with complex objects. In our example, using `characters(name,actor)` we instruct picker to get `name` and `actor` fields inside `characters` key. In other example, we can do something like this: `nextMovie/characters/bornDate(month,year)` to get just `month` and `year` of `bornDate` object inside `characters` from `nextMovie`. 280 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker 2 | -------------------------------------------------------------------------------- /demo/css/style.css: -------------------------------------------------------------------------------- 1 | /* root variables */ 2 | :root { 3 | --bold: 700; 4 | 5 | --spacing-xsmall: 4px; 6 | --spacing-small: calc(var(--spacing-xsmall) * 2); 7 | --spacing-base: 12px; 8 | --spacing-large: calc(var(--spacing-base) + var(--spacing-base)); 9 | 10 | --font-size-small: var(--spacing-base); 11 | --font-size-base: calc(var(--spacing-base) + var(--spacing-xsmall)); 12 | 13 | --black: #000; 14 | --blue: #9565FF; 15 | --gray: #CCC; 16 | --gray-darker: #AAA; 17 | --white: #FFF; 18 | 19 | font-family: monospace; 20 | } 21 | 22 | /* screen */ 23 | .title { text-transform: lowercase;} 24 | 25 | .demo { 26 | display: flex; 27 | flex-direction: row; 28 | justify-content: space-between; 29 | width: 780px; 30 | } 31 | 32 | /* form */ 33 | .field:not(:first-child):not(:last-child) { 34 | margin-top: var(--spacing-large); 35 | } 36 | .field:last-child { margin-top: var(--spacing-xsmall); } 37 | label { 38 | font-size: var(--font-size-base); 39 | font-weight: var(--bold); 40 | } 41 | input, textarea, #js-output { 42 | border-radius: var(--spacing-xsmall); 43 | border: 1px solid var(--gray); 44 | box-shadow: var(--spacing-xsmall) var(--spacing-xsmall) var(--spacing-xsmall) var(--gray) inset; 45 | display: block; 46 | font-family: monospace; 47 | font-size: var(--font-size-small); 48 | outline: none; 49 | padding: var(--spacing-base); 50 | width: 350px; 51 | } 52 | input { font-weight: var(--bold); } 53 | textarea { 54 | height: 150px; 55 | resize: none; 56 | } 57 | input::selection, textarea::selection { 58 | background: var(--black); 59 | color: var(--white); 60 | opacity: 1; 61 | } 62 | #js-run { 63 | background: var(--black); 64 | border-radius: var(--spacing-xsmall); 65 | border: 1px solid var(--gray); 66 | box-shadow: none; 67 | color: var(--white); 68 | cursor: pointer; 69 | font-size: var(--font-size-base); 70 | font-weight: var(--bold); 71 | height: var(--spacing-large); 72 | text-transform: uppercase; 73 | transition: all .2s ease-in-out; 74 | width: 95%; 75 | } 76 | #js-run:hover { 77 | background: var(--gray); 78 | border: 1px solid var(--black); 79 | color: var(--black); 80 | } 81 | 82 | /* console */ 83 | .logger > .content { 84 | background: var(--black); 85 | box-shadow: var(--spacing-xsmall) var(--spacing-xsmall) var(--spacing-xsmall) var(--gray-darker) inset; 86 | color: var(--white); 87 | font-size: var(--font-size-small); 88 | outline: none; 89 | overflow-y: scroll; 90 | padding: var(--spacing-base); 91 | width: 90%; 92 | } 93 | .logger > .content p:first-child { margin: 0; } 94 | .logger > .content::selection { 95 | background: var(--white); 96 | color: var(--black); 97 | } 98 | #js-input { height: 180px; } 99 | #js-output::-webkit-scrollbar { 100 | background-color: var(--gray); 101 | width: var(--spacing-base); 102 | } 103 | #js-output::-webkit-scrollbar-thumb { 104 | border-radius: var(--spacing-base); 105 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); 106 | background-color: var(--gray-darker); 107 | } 108 | #js-output { 109 | cursor: default; 110 | height: 600px; 111 | } 112 | #js-output pre { 113 | display: inline-block; 114 | font-size: var(--font-size-small); 115 | margin: 0; 116 | vertical-align: top; 117 | } 118 | #js-output small { 119 | color: var(--blue); 120 | } 121 | #js-output hr { 122 | border: 0; 123 | border-top: 1px dashed #FFF; 124 | margin: var(--spacing-small) 0; 125 | } 126 | #js-output hr:first-child { display: none; } 127 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Deepicker demo example 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

Deepicker demo example

17 | 18 |
19 |
20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 |
38 |
39 | 40 |
41 | 42 |
43 |
44 | 45 | 49 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/js/interaction.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = { 4 | editor: { 5 | fontSize: 10.5, 6 | mode: 'ace/mode/javascript', 7 | theme: 'ace/theme/monokai', 8 | 9 | initialize: () => { 10 | const { output, submit } = interaction.ui 11 | const editor = ace.edit('js-input') 12 | 13 | submit.addEventListener('click', e => { 14 | e.preventDefault() 15 | config.run(editor.getValue()) 16 | }) 17 | 18 | // TODO: Refactor editor settings values 19 | editor.renderer.setShowGutter(false) 20 | editor.session.setMode(config.editor.mode) 21 | 22 | editor.setFontSize(config.editor.fontSize) 23 | editor.setShowPrintMargin(false) 24 | editor.setTheme(config.editor.theme) 25 | editor.setValue(config.initialValues.defaultScript, 1) 26 | 27 | editor.commands.addCommand({ 28 | name: 'run', 29 | bindKey: { win: 'Ctrl-enter', mac: 'Command-enter' }, 30 | exec: () => config.run(editor.getValue()) 31 | }) 32 | editor.commands.addCommand({ 33 | name: 'clear', 34 | bindKey: { win: 'Ctrl-K', mac: 'Command-K' }, 35 | exec: () => config.writeOn(output, '') 36 | }) 37 | 38 | return config 39 | }, 40 | }, 41 | 42 | initialValues: { 43 | defaultScript: 44 | `const picker = deepicker.simplePicker(include, exclude)\ 45 | \n\nconsole.log(picker.pick(JSON.parse(sampleObject)))`, 46 | exclude: 'bar.foo.other,bar.baz', 47 | include: 'foo,bar', 48 | sampleObject: { 49 | foo: 'bar', 50 | bar: { 51 | baz: 'test', 52 | foo: { 53 | other: 'value', 54 | otherInt: 10, 55 | } 56 | }, 57 | }, 58 | 59 | run: () => { 60 | config.run(config.initialValues.defaultScript) 61 | 62 | return config 63 | }, 64 | }, 65 | 66 | getUI: selector => document.querySelector(selector), 67 | 68 | run: (evalue) => { 69 | config.setVariables() 70 | eval(evalue) 71 | }, 72 | 73 | setVariables: () => { 74 | const { exclude, include, sample } = interaction.ui 75 | 76 | window.exclude = exclude.value 77 | window.include = include.value 78 | window.sampleObject = sample.value 79 | }, 80 | 81 | writeOn: (object, value) => { 82 | if (value) { object.innerHTML += value } else { object.innerHTML = '' } 83 | 84 | return object 85 | } 86 | } 87 | 88 | const interaction = { 89 | ui: { 90 | output: config.getUI('#js-output'), 91 | exclude: config.getUI('#exclude'), 92 | include: config.getUI('#include'), 93 | sample: config.getUI('#sample-object'), 94 | submit: config.getUI('#js-run'), 95 | }, 96 | 97 | initialize: () => { 98 | const { exclude, include, sample } = interaction.ui 99 | 100 | sample.value = JSON.stringify(config.initialValues.sampleObject, null, 2) 101 | include.value = config.initialValues.include 102 | exclude.value = config.initialValues.exclude 103 | 104 | config 105 | .initialValues.run() 106 | .editor.initialize() 107 | }, 108 | 109 | // override console log fn 110 | setupConsole: () => { 111 | if (!console) console = {} 112 | 113 | const { output } = interaction.ui 114 | 115 | const log = (rawMessage, error = false, before = '
>') => { 116 | let updatedMessage = '' 117 | 118 | if (typeof rawMessage === 'object') { 119 | updatedMessage += `
${JSON.stringify(rawMessage, undefined, 2)}
` 120 | } else { updatedMessage = rawMessage } 121 | 122 | config 123 | .writeOn(output, `${before} ${error ? '⚠️' : ''} ${updatedMessage}`) 124 | .scrollTop = output.scrollHeight 125 | } 126 | 127 | console = { 128 | error: (rawMessage) => log(rawMessage, true), 129 | 130 | log: log.bind(this), 131 | } 132 | 133 | window.onerror = (error) => console.error(error) 134 | }, 135 | }; 136 | 137 | (() => { 138 | interaction.setupConsole() 139 | interaction.initialize() 140 | })() 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deepicker", 3 | "version": "1.0.0", 4 | "description": "A small and smart library to filter JS objects 🔪", 5 | "main": "src/index.js", 6 | "homepage": "https://alissonperez.github.io/deepicker/", 7 | "keywords": [ 8 | "js", 9 | "json", 10 | "mask", 11 | "node-js", 12 | "node", 13 | "picker-library", 14 | "picker", 15 | "pick" 16 | ], 17 | "config": { 18 | "path": { 19 | "ignore": [ 20 | "node_modules" 21 | ], 22 | "src": "src" 23 | } 24 | }, 25 | "jest": { 26 | "testURL": "http://localhost" 27 | }, 28 | "scripts": { 29 | "lint": "eslint $npm_package_config_path_src", 30 | "test": "jest" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^6.5.1", 34 | "eslint-loader": "^3.0.2", 35 | "jest": "^24.9.0" 36 | }, 37 | "dependencies": {}, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/alissonperez/deepicker" 41 | }, 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /src/browser/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * deepicker library 3 | * https://alissonperez.github.io/deepicker/ 4 | * 5 | * Copyright JS Foundation and other contributors 6 | * Released under the MIT license 7 | * https://alissonperez.github.io/deepicker/LICENSE 8 | */ 9 | 10 | import deepicker from '../index' 11 | 12 | ((global, factory) => { 13 | 14 | 'use strict' 15 | 16 | if (typeof module === 'object' && typeof module.exports === 'object') { 17 | module.exports = global.document ? 18 | factory(global, true) : 19 | ((w) => { 20 | if (!w.document) { 21 | throw new Error( 'deepicker requires a window with a document' ) 22 | } 23 | return factory(w) 24 | }) 25 | } else { 26 | factory(global) 27 | } 28 | 29 | // Pass this if window is not defined yet 30 | })(typeof window !== 'undefined' ? window : this, (window) => { 31 | // Map over the deepicker in case of overwrite 32 | const _deepicker = window.deepicker 33 | 34 | if (window.deepicker === deepicker) window.deepicker = _deepicker 35 | if (!window.deepicker) window.deepicker = deepicker 36 | }) 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const simpleParser = require('./parsers/simple') 2 | const xpathParser = require('./parsers/xpath') 3 | const picker = require('./picker') 4 | 5 | // TODO: Tests for this module 6 | 7 | function _picker (includeStr, excludeStr, parser) { 8 | if (!includeStr || typeof includeStr !== 'string') { 9 | includeStr = undefined 10 | } 11 | 12 | if (!excludeStr || typeof excludeStr !== 'string') { 13 | excludeStr = undefined 14 | } 15 | 16 | return picker((includeStr && parser(includeStr)) || {}, 17 | (excludeStr && parser(excludeStr)) || {}) 18 | } 19 | 20 | function simplePicker (includeStr, excludeStr) { 21 | return _picker(includeStr, excludeStr, simpleParser) 22 | } 23 | 24 | function xpathPicker (includeStr, excludeStr) { 25 | return _picker(includeStr, excludeStr, xpathParser) 26 | } 27 | 28 | module.exports = { 29 | picker, 30 | xpathPicker, 31 | simplePicker, 32 | } 33 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | const deepicker = require('./index') 2 | 3 | const testVal = { 4 | foo: 'bar', 5 | bar: { 6 | baz: 'test', 7 | foo: { 8 | other: 'value', 9 | otherInt: 10 10 | } 11 | } 12 | } 13 | 14 | describe('#picker', () => { 15 | test('should return raw picker', () => { 16 | const incTree = { 17 | bar: {} 18 | } 19 | 20 | const excTree = { 21 | bar: { 22 | foo: {} 23 | } 24 | } 25 | 26 | const expected = { 27 | bar: { 28 | baz: 'test' 29 | } 30 | } 31 | 32 | expect(deepicker.picker(incTree, excTree).pick(testVal)).toEqual(expected) 33 | }) 34 | }) 35 | 36 | describe('#xpathPicker', () => { 37 | test('should use an xpath inc and exc string', () => { 38 | const include = 'bar/foo,foo', exclude = 'bar/foo/otherInt' 39 | const expected = { 40 | foo: 'bar', 41 | bar: { 42 | foo: { 43 | other: 'value' 44 | } 45 | } 46 | } 47 | 48 | expect(deepicker.xpathPicker(include, exclude).pick(testVal)).toEqual(expected) 49 | }) 50 | }) 51 | 52 | describe('#simplePicker', () => { 53 | test('should use a simple inc and exc string', () => { 54 | const include = 'bar,foo', exclude = 'bar.foo.otherInt,bar.baz' 55 | const expected = { 56 | foo: 'bar', 57 | bar: { 58 | foo: { 59 | other: 'value' 60 | } 61 | } 62 | } 63 | 64 | expect(deepicker.simplePicker(include, exclude).pick(testVal)).toEqual(expected) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/parsers/simple.js: -------------------------------------------------------------------------------- 1 | const util = require('./util') 2 | 3 | function handleWildcard (object) { 4 | if (object['*']) { 5 | Object.keys(object).filter(i => i !== '*').forEach(key => { 6 | util.performMerge(object[key], object['*']) 7 | }) 8 | } 9 | 10 | Object.keys(object).forEach(key => { 11 | handleWildcard(object[key]) 12 | }) 13 | } 14 | 15 | function parseFields (fields) { 16 | let result = {} 17 | 18 | function fill(items, object, pos) { 19 | let item = items[pos] 20 | if (!item) { 21 | return 22 | } 23 | 24 | if (!Object.prototype.hasOwnProperty.call(object, item)) { 25 | object[item] = {} 26 | } 27 | 28 | fill(items, object[item], ++pos) // Go to next 29 | } 30 | 31 | for (let option of fields) { 32 | fill(option.trim().split('.'), result, 0) 33 | } 34 | 35 | handleWildcard(result) 36 | 37 | return result 38 | } 39 | 40 | module.exports = (val) => { 41 | return parseFields(val.split(',')) 42 | } 43 | -------------------------------------------------------------------------------- /src/parsers/simple.test.js: -------------------------------------------------------------------------------- 1 | const simpleParser = require('./simple') 2 | 3 | test('simple parsing "a.b.c"', () => { 4 | const expected = { 5 | 'a': { 6 | 'b': { 7 | 'c': {} 8 | } 9 | } 10 | } 11 | 12 | const val = 'a.b.c' 13 | expect(simpleParser(val)).toEqual(expected) 14 | }) 15 | 16 | test('simple parsing with "a.b.c,a.f,d.e.f"', () => { 17 | const expected = { 18 | 'a': { 19 | 'b': { 20 | 'c': {} 21 | }, 22 | 'f': {} 23 | }, 24 | 'd': { 25 | 'e': { 26 | 'f': {} 27 | } 28 | } 29 | } 30 | 31 | const val = 'a.b.c,a.f,d.e.f' 32 | expect(simpleParser(val)).toEqual(expected) 33 | }) 34 | 35 | test('parsing with *', () => { 36 | const expected = { 37 | 'a': { 38 | '*': { 39 | 'c': {} 40 | } 41 | } 42 | } 43 | 44 | const val = 'a.*.c' 45 | expect(simpleParser(val)).toEqual(expected) 46 | }) 47 | 48 | test('parsing val with * must merge its subtree in other trees', () => { 49 | // This is an optimization one of keys in the same level is * we can merge it all 50 | // in the sub tree of * 51 | 52 | const expected = { 53 | a: { 54 | '*': { 55 | x: {}, 56 | }, 57 | b: { 58 | x: {}, // this must come from '*' neighbor 59 | c: {} 60 | }, 61 | d: { 62 | x: {}, // this must come from '*' neighbor 63 | f: { 64 | g: {} 65 | } 66 | }, 67 | } 68 | } 69 | 70 | const val = 'a.*.x,a.b.c,a.d.f.g' 71 | 72 | expect(simpleParser(val)).toEqual(expected) 73 | }) 74 | -------------------------------------------------------------------------------- /src/parsers/util.js: -------------------------------------------------------------------------------- 1 | // Performs a merge from source into destination 2 | // It's simillar to Object.assign, but merge deeply 3 | // between objects 4 | function performMerge (destination, source) { 5 | Object.keys(source).forEach(key_source => { 6 | if (key_source === '*') { 7 | Object.keys(destination).forEach(destination_key => { 8 | performMerge(destination[destination_key], source[key_source]) 9 | }) 10 | } 11 | 12 | // Adding all * content already set in destination 13 | // At our source before adding it into destination 14 | if (key_source !== '*' && Object.prototype.hasOwnProperty.call(destination, '*')) { 15 | performMerge(source[key_source], destination['*']) 16 | } 17 | 18 | if (!(destination[key_source])) { 19 | destination[key_source] = source[key_source] 20 | return 21 | } 22 | 23 | performMerge(destination[key_source], source[key_source]) 24 | }) 25 | } 26 | 27 | module.exports = { 28 | performMerge 29 | } 30 | -------------------------------------------------------------------------------- /src/parsers/util.spec.js: -------------------------------------------------------------------------------- 1 | const util = require('./util') 2 | 3 | describe('#performMerge', () => { 4 | test('should merge simple objects', () => { 5 | const a = { 6 | foo: {}, 7 | bar: {} 8 | } 9 | 10 | const b = { 11 | other: {}, 12 | testing: {} 13 | } 14 | 15 | util.performMerge(a, b) 16 | 17 | const expected = { 18 | foo: {}, 19 | bar: {}, 20 | other: {}, 21 | testing: {} 22 | } 23 | 24 | expect(a).toEqual(expected) 25 | }) 26 | 27 | test('should merge deeply objects', () => { 28 | const a = { 29 | foo: { 30 | bar: { 31 | baz: {} 32 | } 33 | }, 34 | bar: {} 35 | } 36 | 37 | const b = { 38 | foo: { 39 | other: { 40 | value: {} 41 | } 42 | } 43 | } 44 | 45 | util.performMerge(a, b) 46 | 47 | const expected = { 48 | foo: { 49 | bar: { 50 | baz: {} 51 | }, 52 | other: { 53 | value: {} 54 | } 55 | }, 56 | bar: {} 57 | } 58 | 59 | expect(a).toEqual(expected) 60 | }) 61 | 62 | test('should "update" key with * key if its exists in the first', () => { 63 | const a = { 64 | foo: { 65 | bar: {} 66 | }, 67 | '*': { 68 | testing: {} 69 | } 70 | } 71 | 72 | const b = { 73 | foo: { 74 | other: {} 75 | } 76 | } 77 | 78 | util.performMerge(a, b) 79 | 80 | const expected = { 81 | foo: { 82 | bar: {}, 83 | other: {}, 84 | testing: {} 85 | }, 86 | '*': { 87 | testing: {} 88 | } 89 | } 90 | 91 | expect(a).toEqual(expected) 92 | }) 93 | 94 | test('should "update" key with * key if its exists in the second', () => { 95 | const a = { 96 | foo: { 97 | bar: {} 98 | } 99 | } 100 | 101 | const b = { 102 | foo: { 103 | other: {} 104 | }, 105 | '*': { 106 | testing: {} 107 | } 108 | } 109 | 110 | util.performMerge(a, b) 111 | 112 | const expected = { 113 | foo: { 114 | bar: {}, 115 | other: {}, 116 | testing: {} 117 | }, 118 | '*': { 119 | testing: {} 120 | } 121 | } 122 | 123 | expect(a).toEqual(expected) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /src/parsers/xpath.js: -------------------------------------------------------------------------------- 1 | const util = require('./util') 2 | 3 | const tokens = { 4 | ',': 'COMMA', 5 | '/': 'SLASH', 6 | '(': 'POPEN', 7 | ')': 'PCLOSE', 8 | // We have a FIELD token, but we can't represent it here 9 | } 10 | 11 | // A very simple tokenizer, if grammar increase it's complexity 12 | // it's better to improve it or rewrite. 13 | function tokenizer(text) { 14 | let v, result = [], i = 0, field, startField 15 | 16 | while (i < text.length) { 17 | v = text[i] 18 | 19 | if (v === ' ') { 20 | i++ 21 | continue 22 | } 23 | 24 | // Main tokens 25 | if (Object.prototype.hasOwnProperty.call(tokens, v)) { 26 | result.push({k: tokens[v], v: v, startIndex: i}) 27 | i++ 28 | continue 29 | } 30 | 31 | // If it's not a know token, it only can be a FIELD 32 | field = '' 33 | startField = i 34 | do { 35 | field += v 36 | // Move to next char 37 | v = text[++i] 38 | } while (i < text.length && !Object.prototype.hasOwnProperty.call(tokens, v)) 39 | 40 | result.push({k: 'FIELD', v:field, startIndex: startField}) 41 | } 42 | 43 | return result 44 | } 45 | 46 | // GRAMMAR: 47 | 48 | // FILTERS := FILTER "," FILTERS | FILTER 49 | // FILTER := OBJECT | FIELDPATH 50 | // OBJECT := FIELDPATH "(" FILTERS ")" 51 | // FIELDPATH := FIELD "/" FIELDPATH | FIELD 52 | const parser = { 53 | // This will be overrided for each new object 54 | pos: 0, // Current token position 55 | tokens: [], // Tokens list 56 | 57 | // Return current token 58 | current: function () { 59 | return this.tokens[this.pos] || {} 60 | }, 61 | 62 | // Move to next token 63 | next: function () { 64 | this.pos++ 65 | }, 66 | 67 | currentPos: function () { 68 | return this.pos 69 | }, 70 | 71 | backTrack: function (pos) { 72 | this.pos = pos 73 | }, 74 | 75 | isLastPos: function () { 76 | return this.pos >= (this.tokens.length - 1) 77 | }, 78 | 79 | parse: function () { 80 | const result = this.parseFilters() 81 | //console.log('RESULT', result) 82 | if (!this.isLastPos()) { 83 | this.raiseError() 84 | } 85 | 86 | return result 87 | }, 88 | 89 | parseFilters: function () { 90 | //console.log('CURRENT, parseFilterS', this.current()) 91 | const result = this.parseFilter() 92 | 93 | //console.log('filters result: ', result) 94 | 95 | if (this.current().k === 'COMMA') { 96 | this.next() 97 | const filtersResult = this.parseFilters() 98 | util.performMerge(result, filtersResult || {}) 99 | } 100 | 101 | return result 102 | }, 103 | 104 | parseFilter: function () { 105 | //console.log('CURRENT, parseFilter', this.current()) 106 | // Store here to do a backtracking in case of error 107 | const current = this.currentPos() 108 | 109 | try { 110 | return this.parseObject() 111 | } catch (e) { 112 | this.backTrack(current) 113 | return this.parseFieldPath() 114 | } 115 | }, 116 | 117 | parseObject: function () { 118 | //console.log('CURRENT, parseObject', this.current()) 119 | const result = this.parseFieldPath() 120 | 121 | if (this.current().k === 'POPEN') { 122 | this.next() 123 | const filtersResult = this.parseFilters() 124 | 125 | // We need to set it to last position of our tree 126 | const addToResult = (resultItem) => { 127 | const key = Object.keys(resultItem)[0] 128 | if (Object.keys(resultItem[key]).length === 0) { 129 | resultItem[key] = filtersResult 130 | return 131 | } 132 | 133 | addToResult(resultItem[key]) 134 | } 135 | 136 | addToResult(result) 137 | 138 | if (this.current().k === 'PCLOSE') { 139 | this.next() 140 | return result 141 | } 142 | 143 | this.raiseError() 144 | } 145 | 146 | this.raiseError() 147 | }, 148 | 149 | parseFieldPath: function () { 150 | //console.log('CURRENT, parseFieldPath', this.current()) 151 | if (this.current().k === 'FIELD') { 152 | const key = this.current().v 153 | const result = { 154 | [key]: {} 155 | } 156 | 157 | this.next() 158 | 159 | if (this.current().k === 'SLASH') { 160 | this.next() 161 | 162 | result[key] = this.parseFieldPath() 163 | } 164 | 165 | return result 166 | } 167 | 168 | this.raiseError() 169 | }, 170 | 171 | raiseError: function () { 172 | throw Error('Unexpected token ' + this.current().k + ' with value "' + this.current().v + '" at position ' + this.current().startIndex) 173 | } 174 | } 175 | 176 | function parse (val) { 177 | const p = Object.create(parser) 178 | p.tokens = tokenizer(val) 179 | return p.parse() 180 | } 181 | 182 | module.exports = parse 183 | -------------------------------------------------------------------------------- /src/parsers/xpath.test.js: -------------------------------------------------------------------------------- 1 | const xPathParser = require('./xpath') 2 | 3 | test('simple parsing "a/b/c"', () => { 4 | const expected = { 5 | 'a': { 6 | 'b': { 7 | 'c': {} 8 | } 9 | } 10 | } 11 | 12 | const val = 'a/b/c' 13 | expect(xPathParser(val)).toEqual(expected) 14 | }) 15 | 16 | test('parsing complex "a,b(c/d,e/f)', () => { 17 | const expected = { 18 | 'a': {}, 19 | 'b': { 20 | 'c': { 21 | 'd': {} 22 | }, 23 | 'e': { 24 | 'f': {} 25 | } 26 | } 27 | } 28 | 29 | const val = 'a,b(c/d,e/f)' 30 | expect(xPathParser(val)).toEqual(expected) 31 | }) 32 | 33 | test('parsing val that needs a deep merge', () => { 34 | const expected = { 35 | 'a': { 36 | 'b': {}, 37 | 'c': {}, 38 | 'd': {}, 39 | 'f': {}, 40 | } 41 | } 42 | 43 | const val = 'a(b,c),a(d,f)' 44 | expect(xPathParser(val)).toEqual(expected) 45 | }) 46 | 47 | test('parsing val with * must merge its subtree in other trees', () => { 48 | // This is an optimization one of keys in the same level is * we can merge it all 49 | // in the sub tree of * 50 | 51 | const expected = { 52 | a: { 53 | '*': { 54 | x: {}, 55 | }, 56 | b: { 57 | x: {}, // this must come from '*' neighbor 58 | c: {} 59 | }, 60 | d: { 61 | x: {}, // this must come from '*' neighbor 62 | f: { 63 | g: {} 64 | } 65 | }, 66 | } 67 | } 68 | 69 | const val = 'a/*/x,a/b/c,a/d/f/g' 70 | 71 | expect(xPathParser(val)).toEqual(expected) 72 | }) 73 | -------------------------------------------------------------------------------- /src/picker.js: -------------------------------------------------------------------------------- 1 | 2 | // Utility to test if a givem param is a function 3 | function isFunction(functionToCheck) { 4 | var getType = {} 5 | return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]' 6 | } 7 | 8 | function newPicker (incTree, excTree) { 9 | const localPicker = Object.create(picker) 10 | 11 | localPicker.incTree = incTree || {} 12 | localPicker.excTree = excTree || {} 13 | 14 | return localPicker 15 | } 16 | 17 | const picker = { 18 | // Include and exclude trees 19 | incTree: {}, 20 | excTree: {}, 21 | 22 | toContext: function() { 23 | let incTree = this.incTree || {} 24 | let excTree = this.excTree || {} 25 | 26 | Array.from(arguments).forEach(item => { 27 | incTree = incTree[item] || incTree['*'] || {} 28 | excTree = excTree[item] || excTree['*'] || {} 29 | }) 30 | 31 | return newPicker(incTree, excTree) 32 | }, 33 | 34 | include: function (key) { 35 | return this._include(key, this.incTree, this.excTree) 36 | }, 37 | 38 | _include: function (key, incTree, excTree) { 39 | incTree = incTree || {} 40 | excTree = excTree || {} 41 | 42 | // It is in include keys, must be included 43 | if (Object.prototype.hasOwnProperty.call(incTree, key)) { 44 | return true 45 | } 46 | 47 | // Key isn't in include keys but its filled, 48 | // it can't be included 49 | if (Object.keys(incTree).length > 0 50 | && !Object.prototype.hasOwnProperty.call(incTree, '*')) { 51 | return false 52 | } 53 | 54 | // It is explicity in exclude keys and exclude keys is empty (is a leaf), 55 | // must not be included 56 | if (Object.prototype.hasOwnProperty.call(excTree, key) 57 | && Object.keys(excTree[key]).length === 0) { 58 | return false 59 | } 60 | 61 | // Handle "*" wildcard in include 62 | if (Object.prototype.hasOwnProperty.call(incTree, '*')) { 63 | return true 64 | } 65 | 66 | // Handle "*" wildcard in exclude 67 | if (Object.prototype.hasOwnProperty.call(excTree, '*') 68 | && Object.keys(excTree['*']).length === 0) { 69 | return false 70 | } 71 | 72 | return true 73 | }, 74 | 75 | pickStatic: function (val) { 76 | return this._pickStatic(val, this.incTree, this.excTree) 77 | }, 78 | 79 | _pickStatic: function (val, incTree, excTree) { 80 | // Leafs (if we reach there) 81 | if (typeof val !== 'object' || val === null || val === undefined) { 82 | return val 83 | } 84 | 85 | incTree = incTree || {} 86 | excTree = excTree || {} 87 | 88 | let keys = Object.keys(incTree) 89 | if (keys.length === 0 && Object.keys(excTree).length === 0) { 90 | // Nothing to do, a fast return with all value left 91 | return val 92 | } 93 | 94 | // Handle arrays 95 | if (val instanceof Array) { 96 | return val.map(item => this._pickStatic(item, incTree, excTree)) 97 | } 98 | 99 | if (keys.length === 0 || keys.indexOf('*') > -1) { 100 | keys = Object.keys(val) 101 | } 102 | 103 | // Result object 104 | const result = {} 105 | 106 | keys.forEach(key => { 107 | if (!Object.prototype.hasOwnProperty.call(val, key)) { 108 | return 109 | } 110 | 111 | if (!(this._include(key, incTree, excTree))) { 112 | return 113 | } 114 | 115 | result[key] = this._pickStatic( 116 | val[key], 117 | incTree[key] || incTree['*'], 118 | excTree[key] || excTree['*'] 119 | ) 120 | }) 121 | 122 | return result 123 | }, 124 | 125 | pickPromise: function (val) { 126 | return this._pick(val, this.incTree, this.excTree, true) 127 | }, 128 | 129 | pick: function (val) { 130 | return this._pick(val, this.incTree, this.excTree, false) 131 | }, 132 | 133 | _pick: function (val, incTree, excTree, promise) { 134 | // Simple values 135 | if (typeof val === 'number' || 136 | typeof val === 'string' || 137 | val === null || 138 | val === undefined) { 139 | return val 140 | } 141 | 142 | incTree = incTree || {} 143 | excTree = excTree || {} 144 | 145 | // Promises (if we handle it) 146 | if (promise && val && typeof val.then === 'function') { 147 | return val.then(promiseResult => { 148 | return this._pick(promiseResult, incTree, excTree, promise) 149 | }) 150 | } 151 | 152 | // A function 153 | if (isFunction(val)) { 154 | return val(newPicker(incTree, excTree)) 155 | } 156 | 157 | // An array TODO Check how handle promises here 158 | if (val instanceof Array) { 159 | if (promise) { 160 | return Promise.all(val.map(item => this._pick(item, incTree, excTree, promise))) 161 | } 162 | 163 | return val.map(item => this._pick(item, incTree, excTree, promise)) 164 | } 165 | 166 | // Objects 167 | const result = {} 168 | const promises = [] 169 | 170 | let keys = Object.keys(incTree) 171 | if (keys.length === 0 || keys.indexOf('*') > -1) { 172 | keys = Object.keys(val) 173 | } 174 | 175 | let key 176 | for (let i in keys) { 177 | key = keys[i] 178 | if (!Object.prototype.hasOwnProperty.call(val, key)) continue 179 | 180 | if (!(this._include(key, incTree, excTree))) { 181 | continue 182 | } 183 | 184 | // Solve key 185 | result[key] = this._pick( 186 | val[key], 187 | incTree[key] || incTree['*'], 188 | excTree[key] || excTree['*'], 189 | promise 190 | ) 191 | 192 | if (promise && result[key] && typeof result[key].then === 'function') { 193 | promises.push(key, result[key]) 194 | } 195 | } 196 | 197 | // Wait promises if we handle it 198 | if (promise && promises.length > 0) { 199 | return Promise.all(promises).then(promisesResult => { 200 | // Iterating over pair of key and result 201 | for (let i=0; i < promisesResult.length; i += 2) { 202 | result[promisesResult[i]] = promisesResult[i+1] 203 | } 204 | 205 | return result 206 | }) 207 | } 208 | 209 | return promise ? Promise.resolve(result) : result 210 | } 211 | } 212 | 213 | module.exports = newPicker 214 | -------------------------------------------------------------------------------- /src/picker.test.js: -------------------------------------------------------------------------------- 1 | const picker = require('./picker') 2 | 3 | describe('#pick', () => { 4 | const defaultIncTree = { 5 | foo: { 6 | bar: {} 7 | } 8 | } 9 | 10 | test('should return all object if trees are empty', () => { 11 | const val = { 12 | foo: { 13 | bar: 3 14 | }, 15 | other: { 16 | foo: 'bar' 17 | } 18 | } 19 | 20 | expect(picker().pick(val)).toEqual(val) 21 | expect(picker({}, {}).pick(val)).toEqual(val) 22 | }) 23 | 24 | test('should filter simple objects', () => { 25 | const val = { 26 | foo: { 27 | baz: 2, 28 | bar: 3, 29 | test: { 30 | val: 'test' 31 | } 32 | }, 33 | other: { 34 | foo: 'bar' 35 | } 36 | } 37 | 38 | const expected = { 39 | foo: { 40 | bar: 3 41 | } 42 | } 43 | 44 | expect(picker(defaultIncTree).pick(val)).toEqual(expected) 45 | }) 46 | 47 | test('should filter arrays', () => { 48 | const val = { 49 | foo: [ 50 | {bar: 1, baz: 2}, 51 | {bar: 3, baz: 4}, 52 | {bar: 5, baz: 6}, 53 | ] 54 | } 55 | 56 | const expected = { 57 | foo: [ 58 | {bar: 1}, 59 | {bar: 3}, 60 | {bar: 5}, 61 | ] 62 | } 63 | 64 | expect(picker(defaultIncTree).pick(val)).toEqual(expected) 65 | }) 66 | 67 | test('should filter arrays with exclude', () => { 68 | const val = { 69 | foo: { 70 | myArray: [ 71 | {bar: 1, baz: 2}, 72 | {bar: 3, baz: 4}, 73 | {bar: 5, baz: 6}, 74 | ] 75 | } 76 | } 77 | 78 | const excTree = { 79 | foo: { 80 | myArray: { 81 | baz: {} 82 | } 83 | } 84 | } 85 | 86 | const expected = { 87 | foo: { 88 | myArray: [ 89 | {bar: 1}, 90 | {bar: 3}, 91 | {bar: 5}, 92 | ] 93 | } 94 | } 95 | 96 | expect(picker({}, excTree).pick(val)).toEqual(expected) 97 | }) 98 | 99 | test('should call functions with expected context', () => { 100 | const val = { 101 | foo: function (picker) { 102 | return picker.pick({ 103 | bar: 1, 104 | baz: 2 105 | }) 106 | }, 107 | other: 'test' 108 | } 109 | 110 | const expected = { 111 | foo: { 112 | bar: 1 113 | } 114 | } 115 | 116 | expect(picker(defaultIncTree).pick(val)).toEqual(expected) 117 | }) 118 | 119 | test('should not return items in exclude tree', () => { 120 | const val = { 121 | foo: { 122 | baz: 2, 123 | bar: 3, 124 | test: { 125 | val: 'test' 126 | } 127 | }, 128 | other: { 129 | foo: 'bar' 130 | } 131 | } 132 | 133 | const expected = { 134 | foo: { 135 | baz: 2, 136 | bar: 3, 137 | test: { 138 | val: 'test' 139 | } 140 | }, 141 | other: {} 142 | } 143 | 144 | const excTree = { 145 | other: { 146 | foo: {} 147 | } 148 | } 149 | 150 | expect(picker({}, excTree).pick(val)).toEqual(expected) 151 | }) 152 | 153 | test('should return keys mixed between include and exclude', () => { 154 | const val = { 155 | foo: { 156 | baz: 2, 157 | bar: 3, 158 | test: { 159 | val: 'test' 160 | } 161 | }, 162 | other: { 163 | foo: 'bar', 164 | test: 'foo' 165 | } 166 | } 167 | 168 | const incTree = { 169 | foo: { 170 | bar: {} 171 | }, 172 | other: {} 173 | } 174 | 175 | const excTree = { 176 | other: { 177 | foo: {} 178 | } 179 | } 180 | 181 | const expected = { 182 | foo: { 183 | bar: 3 184 | }, 185 | other: { 186 | test: 'foo' 187 | } 188 | } 189 | 190 | expect(picker(incTree, excTree).pick(val)).toEqual(expected) 191 | }) 192 | 193 | test('should resolve a complex object', () => { 194 | const val = { 195 | foo: 'bar', 196 | aNumber: 1, 197 | test: { 198 | foo: 'baz' 199 | }, 200 | other: function (picker) { 201 | return picker.pick({ 202 | foo: function (picker) { 203 | return picker.pick({ 204 | test: 'foo', 205 | toExclude: 'bar' 206 | }) 207 | } 208 | }) 209 | }, 210 | aSimpleArray: [1, 2, 3, 4], 211 | anArrayWithObject: [ 212 | {bar: 'baz', foo: 'bar'}, 213 | {bar: 'baz', foo: 'bar'}, 214 | {bar: 'baz', foo: 'bar'}, 215 | ], 216 | } 217 | 218 | const incTree = { 219 | foo: {}, 220 | aNumber: {}, 221 | test: {}, 222 | other: {}, 223 | aSimpleArray: {}, 224 | anArrayWithObject: {}, 225 | } 226 | 227 | const excTree = { 228 | other: { 229 | foo: { 230 | toExclude: {} 231 | } 232 | } 233 | } 234 | 235 | const expected = { 236 | foo: 'bar', 237 | aNumber: 1, 238 | test: { 239 | foo: 'baz' 240 | }, 241 | other: { 242 | foo: { 243 | test: 'foo' 244 | } 245 | }, 246 | aSimpleArray: [1, 2, 3, 4], 247 | anArrayWithObject: [ 248 | {bar: 'baz', foo: 'bar'}, 249 | {bar: 'baz', foo: 'bar'}, 250 | {bar: 'baz', foo: 'bar'}, 251 | ], 252 | } 253 | 254 | expect(picker(incTree, excTree).pick(val)).toEqual(expected) 255 | }) 256 | 257 | test('picker with * field in include fields', () => { 258 | const val = { 259 | foo: { 260 | test1: { 261 | val: 'test', 262 | val2: 'test2' 263 | }, 264 | test2: { 265 | val: 'test', 266 | val2: 'test2' 267 | } 268 | } 269 | } 270 | 271 | const expected = { 272 | foo: { 273 | test1: { 274 | val2: 'test2' 275 | }, 276 | test2: { 277 | val2: 'test2' 278 | } 279 | } 280 | } 281 | 282 | const incTree = { 283 | foo: { 284 | '*': { 285 | 'val2': {} 286 | } 287 | } 288 | } 289 | 290 | expect(picker(incTree).pick(val)).toEqual(expected) 291 | }) 292 | 293 | test('picker with * field in exclude fields', () => { 294 | const val = { 295 | foo: { 296 | test1: { 297 | val: 'test', 298 | val2: 'test2' 299 | }, 300 | test2: { 301 | val: 'test', 302 | val2: 'test2' 303 | } 304 | } 305 | } 306 | 307 | const expected = { 308 | foo: { 309 | test1: { 310 | val2: 'test2' 311 | }, 312 | test2: { 313 | val2: 'test2' 314 | } 315 | } 316 | } 317 | 318 | const excTree = { 319 | foo: { 320 | '*': { 321 | 'val': {} 322 | } 323 | } 324 | } 325 | 326 | expect(picker({}, excTree).pick(val)).toEqual(expected) 327 | }) 328 | }) 329 | 330 | describe('#pickPromise', () => { 331 | test('should resolve a simple object with promise', () => { 332 | const val = { 333 | foo: 'bar', 334 | myPromise: Promise.resolve('solved') 335 | } 336 | 337 | const incTree = { 338 | foo: {}, 339 | myPromise: {} 340 | } 341 | 342 | const expected = { 343 | foo: 'bar', 344 | myPromise: 'solved' 345 | } 346 | 347 | return picker(incTree).pickPromise(val).then(result => { 348 | return expect(result).toEqual(expected) 349 | }) 350 | }) 351 | 352 | test('should solve multiple promises', () => { 353 | const val = { 354 | foo: 'bar', 355 | myPromise: Promise.resolve('solved'), 356 | myOtherLazyFunc: Promise.resolve('solved again') 357 | } 358 | 359 | const incTree = { 360 | foo: {}, 361 | myPromise: {}, 362 | myOtherLazyFunc: {} 363 | } 364 | 365 | const expected = { 366 | foo: 'bar', 367 | myPromise: 'solved', 368 | myOtherLazyFunc: 'solved again' 369 | } 370 | 371 | return picker(incTree).pickPromise(val).then(result => { 372 | return expect(result).toEqual(expected) 373 | }) 374 | }) 375 | 376 | test('should resolve a promise inside a function', () => { 377 | const val = { 378 | foo: 'bar', 379 | myPromise: function() { 380 | return Promise.resolve('solved') 381 | } 382 | } 383 | 384 | const incTree = { 385 | foo: {}, 386 | myPromise: {} 387 | } 388 | 389 | const expected = { 390 | foo: 'bar', 391 | myPromise: 'solved' 392 | } 393 | 394 | return picker(incTree).pickPromise(val).then(result => { 395 | return expect(result).toEqual(expected) 396 | }) 397 | }) 398 | 399 | test('should resolve a function pick method', () => { 400 | const val = { 401 | foo: function (picker) { 402 | return picker.pick({ 403 | bar: 'baz', 404 | other: 'value' 405 | }) 406 | } 407 | } 408 | 409 | const incTree = { 410 | foo: { 411 | bar: {} 412 | } 413 | } 414 | 415 | const expected = { 416 | foo: { 417 | bar: 'baz' 418 | } 419 | } 420 | 421 | return picker(incTree).pickPromise(val).then(result => { 422 | return expect(result).toEqual(expected) 423 | }) 424 | }) 425 | 426 | test('should resolve an array of promises', () => { 427 | const val = { 428 | foo: [ 429 | Promise.resolve({ 430 | bar: 'baz', 431 | other: 'value' 432 | }), 433 | Promise.resolve({ 434 | bar: 'other baz', 435 | other: 'value' 436 | }) 437 | ] 438 | } 439 | 440 | const incTree = { 441 | foo: { 442 | bar: {} 443 | } 444 | } 445 | 446 | const expected = { 447 | foo: [ 448 | {bar: 'baz'}, 449 | {bar: 'other baz'}, 450 | ] 451 | } 452 | 453 | return picker(incTree).pickPromise(val).then(result => { 454 | return expect(result).toEqual(expected) 455 | }) 456 | }) 457 | }) 458 | 459 | describe('#include', () => { 460 | test('should return true if both inc and exc tree is undefined', () => { 461 | expect(picker().include('foo')).toBe(true) 462 | }) 463 | 464 | test('should return true if both inc and exc is empty', () => { 465 | expect(picker({}, {}).include('foo')).toBe(true) 466 | }) 467 | 468 | test('should return true if key is included', () => { 469 | const incTree = { 470 | foo: {}, 471 | myPromise: {} 472 | } 473 | 474 | expect(picker(incTree).include('foo')).toBe(true) 475 | }) 476 | 477 | test('should return true if include has "*" wildcard', () => { 478 | const incTree = { 479 | foo: {}, 480 | '*': {} 481 | } 482 | 483 | expect(picker(incTree).include('other')).toBe(true) 484 | }) 485 | 486 | test('should return false if key is not in include', () => { 487 | const incTree = { 488 | foo: {}, 489 | myPromise: {} 490 | } 491 | 492 | expect(picker(incTree).include('bar')).toBe(false) 493 | }) 494 | 495 | test('should return false if key is in exclude', () => { 496 | const incTree = {} 497 | 498 | const excTree = { 499 | foo: {}, 500 | myPromise: {} 501 | } 502 | 503 | expect(picker(incTree, excTree).include('myPromise')).toBe(false) 504 | }) 505 | 506 | test('should return true if key is in exclude and include', () => { 507 | const incTree = { 508 | foo: {}, 509 | myPromise: {} 510 | } 511 | 512 | const excTree = { 513 | foo: {} 514 | } 515 | 516 | expect(picker(incTree, excTree).include('foo')).toBe(true) 517 | }) 518 | 519 | test('should return true if key is in exclude but has sub keys to exclude', () => { 520 | const excTree = { 521 | foo: { 522 | bar: {} 523 | } 524 | } 525 | 526 | expect(picker({}, excTree).include('foo')).toBe(true) 527 | }) 528 | 529 | test('should return false if exclude has "*" wildcard', () => { 530 | const incTree = {} 531 | 532 | const excTree = { 533 | '*': {} 534 | } 535 | 536 | expect(picker(incTree, excTree).include('other')).toBe(false) 537 | }) 538 | 539 | test('should return true if exclude has "*" wildcard but has sub tree', () => { 540 | const incTree = {} 541 | 542 | const excTree = { 543 | '*': { 544 | foo: {} 545 | } 546 | } 547 | 548 | expect(picker(incTree, excTree).include('foo')).toBe(true) 549 | }) 550 | 551 | test('should return true if include has exact key and exclude has "*" wildcard', () => { 552 | const incTree = { 553 | foo: {} 554 | } 555 | 556 | const excTree = { 557 | '*': {} 558 | } 559 | 560 | expect(picker(incTree, excTree).include('foo')).toBe(true) 561 | }) 562 | }) 563 | 564 | describe('#pickStatic', () => { 565 | test('should return same json', () => { 566 | const val = { 567 | foo: { 568 | bar: 3 569 | }, 570 | other: { 571 | foo: 'bar' 572 | } 573 | } 574 | 575 | expect(picker().pickStatic(val)).toEqual(val) 576 | }) 577 | 578 | test('should return json filtered', () => { 579 | const val = { 580 | foo: { 581 | bar: 3 582 | }, 583 | other: { 584 | foo: 'bar' 585 | } 586 | } 587 | 588 | const incTree = { 589 | foo: {}, 590 | } 591 | 592 | const expected = { 593 | foo: { 594 | bar: 3 595 | } 596 | } 597 | 598 | expect(picker(incTree).pickStatic(val)).toEqual(expected) 599 | }) 600 | 601 | test('should handle arrays', () => { 602 | const val = { 603 | foo: [ 604 | {bar: 'baz', other: 'value'}, 605 | {bar: 'baz', other: 'value'}, 606 | {bar: 'baz', other: 'value'} 607 | ], 608 | other: { 609 | foo: 'bar' 610 | } 611 | } 612 | 613 | const incTree = { 614 | foo: { 615 | bar: {} 616 | } 617 | } 618 | 619 | const expected = { 620 | foo: [ 621 | {bar: 'baz'}, 622 | {bar: 'baz'}, 623 | {bar: 'baz'} 624 | ] 625 | } 626 | 627 | expect(picker(incTree).pickStatic(val)).toEqual(expected) 628 | }) 629 | 630 | test('should exclude some fields', () => { 631 | const val = { 632 | foo: { 633 | bar: 3, 634 | other: 'value' 635 | }, 636 | other: { 637 | foo: 'bar' 638 | } 639 | } 640 | 641 | const incTree = { 642 | foo: {}, 643 | other: {} 644 | } 645 | 646 | const excTree = { 647 | foo: { 648 | other: {} 649 | } 650 | } 651 | 652 | const expected = { 653 | foo: { 654 | bar: 3, 655 | }, 656 | other: { 657 | foo: 'bar' 658 | } 659 | } 660 | 661 | expect(picker(incTree, excTree).pickStatic(val)).toEqual(expected) 662 | }) 663 | 664 | test('include should have priority over exclude if same key is in both', () => { 665 | const val = { 666 | foo: { 667 | bar: 3, 668 | other: 'value' 669 | }, 670 | other: { 671 | foo: 'bar' 672 | } 673 | } 674 | 675 | const incTree = { 676 | foo: { 677 | bar: {} 678 | } 679 | } 680 | 681 | const excTree = incTree 682 | 683 | const expected = { 684 | foo: { 685 | bar: 3 686 | } 687 | } 688 | 689 | expect(picker(incTree, excTree).pickStatic(val)).toEqual(expected) 690 | }) 691 | 692 | test('should handle "*" wild card in include tree', () => { 693 | const val = { 694 | foo: { 695 | bar: { 696 | baz: 'foo 1', 697 | toBeRemoved: 'a value' 698 | }, 699 | other: { 700 | baz: 'foo 2', 701 | otherKey: 'with value' 702 | } 703 | }, 704 | other: { 705 | myKey: 'foo' 706 | } 707 | } 708 | 709 | const incTree = { 710 | foo: { 711 | '*': { 712 | baz: {} 713 | } 714 | } 715 | } 716 | 717 | const expected = { 718 | foo: { 719 | bar: { 720 | baz: 'foo 1' 721 | }, 722 | other: { 723 | baz: 'foo 2' 724 | } 725 | } 726 | } 727 | 728 | expect(picker(incTree).pickStatic(val)).toEqual(expected) 729 | }) 730 | 731 | test('should handle "*" wild card in exclude tree', () => { 732 | const val = { 733 | foo: { 734 | bar: { 735 | baz: 'foo 1', 736 | toBeRemoved: 'a value' 737 | }, 738 | other: { 739 | baz: 'foo 2', 740 | otherKey: 'with value' 741 | } 742 | }, 743 | other: { 744 | myKey: 'foo' 745 | } 746 | } 747 | 748 | const excTree = { 749 | foo: { 750 | '*': { 751 | baz: {} 752 | } 753 | } 754 | } 755 | 756 | const expected = { 757 | foo: { 758 | bar: { 759 | toBeRemoved: 'a value' 760 | }, 761 | other: { 762 | otherKey: 'with value' 763 | } 764 | }, 765 | other: { 766 | myKey: 'foo' 767 | } 768 | } 769 | 770 | expect(picker({}, excTree).pickStatic(val)).toEqual(expected) 771 | }) 772 | 773 | test('should handle null and undefined', () => { 774 | const tree = { 775 | foo: {} 776 | } 777 | 778 | expect(picker(tree).pickStatic(null)).toBe(null) 779 | expect(picker(tree).pickStatic(undefined)).toBe(undefined) 780 | }) 781 | }) 782 | 783 | describe('#toContext', () => { 784 | test('move to context tree', () => { 785 | const incTree = { 786 | foo: { 787 | bar: { 788 | baz: {}, 789 | other: { 790 | key: {} 791 | } 792 | } 793 | } 794 | } 795 | 796 | const excTree = { 797 | foo: { 798 | bar: { 799 | other: { 800 | key: {} 801 | } 802 | } 803 | } 804 | } 805 | 806 | const localPicker = picker(incTree, excTree).toContext('foo', 'bar') 807 | 808 | expect(localPicker.incTree).toEqual(incTree.foo.bar) 809 | expect(localPicker.excTree).toEqual(excTree.foo.bar) 810 | }) 811 | 812 | test('move to context should handle empty trees', () => { 813 | const localPicker = picker().toContext('foo', 'bar') 814 | 815 | expect(localPicker.incTree).toEqual({}) 816 | expect(localPicker.excTree).toEqual({}) 817 | }) 818 | 819 | test('move context should move through "*" wildcard', () => { 820 | const incTree = { 821 | foo: { 822 | '*': { 823 | baz: {}, 824 | other: { 825 | key: {} 826 | } 827 | }, 828 | other: { 829 | foo: {} 830 | } 831 | } 832 | } 833 | 834 | const excTree = { 835 | foo: { 836 | '*': { 837 | other: { 838 | key: {} 839 | } 840 | } 841 | } 842 | } 843 | 844 | const localPicker = picker(incTree, excTree).toContext('foo', 'bar') 845 | 846 | expect(localPicker.incTree).toEqual(incTree.foo['*']) 847 | expect(localPicker.excTree).toEqual(excTree.foo['*']) 848 | }) 849 | }) 850 | --------------------------------------------------------------------------------