├── .eslintrc.js ├── .flowconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ └── codeql.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── sonar-project.properties └── t ├── 1-filter.test.js ├── 2-exist.test.js ├── 2-merge.test.js └── 3-ArrayIter.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'es6': true, 4 | 'node': true 5 | }, 6 | 'extends': 'eslint:recommended', 7 | 'parserOptions': { 8 | 'ecmaVersion': 2017, 9 | 'sourceType': 'module' 10 | }, 11 | 'rules': { 12 | 'indent': [ 13 | 'error', 14 | 'tab', 15 | { 'SwitchCase': 0 }, 16 | ], 17 | 'linebreak-style': [ 18 | 'error', 19 | 'unix' 20 | ], 21 | 'quotes': [ 22 | 'error', 23 | 'double' 24 | ], 25 | 'semi': [ 26 | 'error', 27 | 'always' 28 | ], 29 | 'no-console': [ 30 | 'warn', 31 | { allow: ['warn', 'error', 'info'] } 32 | ], 33 | 'brace-style': [ 34 | 'error', 35 | '1tbs', 36 | { 'allowSingleLine': true } 37 | ], 38 | 'no-unused-vars': [ 39 | 'warn', 40 | { 41 | 'vars': 'all', 42 | 'args': 'after-used', 43 | 'ignoreRestSiblings': false 44 | } 45 | ], 46 | 'no-mixed-spaces-and-tabs': [ 47 | 'error', 48 | 'smart-tabs' 49 | ] 50 | } 51 | }; -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: bluet 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | jobs: 9 | sonarcloud: 10 | name: SonarCloud 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Install dependencies 17 | run: npm i 18 | - name: Test and coverage 19 | run: npm run test 20 | - name: SonarCloud Scan 21 | uses: SonarSource/sonarcloud-github-action@master 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | SONAR_TOKEN: ${{ secrets. SONAR_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "58 9 * * 1" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .travis.yml 40 | sonar-project.properties 41 | .scannerwork/ 42 | .flowconfig 43 | .eslintrc.js 44 | *.tgz 45 | .github/ 46 | t/ 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "10" 5 | - "12" 6 | - "14" 7 | 8 | dist: xenial 9 | sudo: required 10 | 11 | addons: 12 | sonarqube: 13 | organization: "bluet-github" 14 | token: 15 | secure: "y5E0d15ntItxYRV5O6DHtAGm+W7hI4jIpbkr+uKk4ZO/KsEVML9mhVgmUGrrx1V9Zbgzy+FBpL84dPqNKVJGfo5/aoTMNBdoogG7GbcGiS9A0ReZlJmmdgdpZeOqBCHNlMJmx+hcBlVD+hmrNGY0o6DujllDS37jjA0U1WDyJJZIud4Tp5ifHxuyxNsvyXu6CFTXaWgPsM26g8U+cCAU51A2/uFVZcmrZ65R/AQ1PkHfSOq8RRHFaDGXmOMypw6rolB0cE5iExhdaxUbQ/V1vN5oo/wZxPzBO3ok6q7kbznrdJVMvnDA1OeZDoOgZa+tul0BXZxsL8iry2QswHllyOK2E2/uqIv6n/QMvVFQ3VEFcQxsYyO02puqxBd5wXd4I/tZllq/2PXGFCj3XEHFa8qsIZUmiUFdF+be+SOAJFTTatrg6h8QLe0Zm2vkVdPNJmVufN9wDddvThgI9JMYGHq0IFVLueIEZ4y2OhYt36epRcjJUds7SPDvaW35yc6mmznLxSdxUvr3jEzNmFh+oII4WYSl25ekPX9ME86zGAs0UmNDHCdK2kb8KwnW8M81pH1oGKhTVDZ37uvBEhVMVqNOahN/BY/HWCrIKJ0CjvwpU2KiD5wf7/NIKOtTSQhv1FxXGtNoplaOa5vXmcKedRkVFhwCfUgnIdjTDvdUhE0=" 16 | jdk: 17 | - oraclejdk8 18 | script: 19 | - npm test 20 | - sonar-scanner 21 | cache: 22 | directories: 23 | - '$HOME/.sonar/cache' 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 BlueT - Matthew Lien - 練喆明 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/bluet/obj-filter/actions/workflows/sonarcloud.yml/badge.svg)](https://github.com/bluet/obj-filter/actions/workflows/sonarcloud.yml) 2 | [![CodeQL](https://github.com/bluet/obj-filter/actions/workflows/codeql.yml/badge.svg)](https://github.com/bluet/obj-filter/actions/workflows/codeql.yml) 3 | [![Node CI](https://github.com/bluet/obj-filter/actions/workflows/nodejs.yml/badge.svg)](https://github.com/bluet/obj-filter/actions/workflows/nodejs.yml) 4 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fbluet%2Fobj-filter.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fbluet%2Fobj-filter?ref=badge_shield) 5 | 6 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=obj-filter&metric=alert_status)](https://sonarcloud.io/dashboard?id=obj-filter) 7 | [![Open Source Helpers](https://www.codetriage.com/bluet/obj-filter/badges/users.svg)](https://www.codetriage.com/bluet/obj-filter) 8 | [![npm version](https://img.shields.io/npm/v/obj-filter.svg)](https://www.npmjs.org/package/obj-filter) 9 | [![install size](https://packagephobia.now.sh/badge?p=obj-filter)](https://packagephobia.now.sh/result?p=obj-filter) 10 | [![npm downloads](https://img.shields.io/npm/dm/obj-filter.svg)](http://npm-stat.com/charts.html?package=obj-filter) 11 | [![GitHub license](https://img.shields.io/github/license/BlueT/obj-filter.svg)](https://github.com/BlueT/obj-filter/blob/master/LICENSE) 12 | 13 | # obj-filter - JavaScript Object Filter / Merger. 14 | 15 | JavaScript Object Filter. **Deep** filtering key/content *recursively*. 16 | Support **type checking**, **wildcard**, **nested**, and **filter function** in *template*. 17 | 18 | ## INSTALL 19 | 20 | `npm i obj-filter` 21 | 22 | Or find help from: 23 | - https://www.npmjs.com/package/obj-filter 24 | - https://github.com/BlueT/obj-filter 25 | 26 | ## SYNOPSIS 27 | 28 | ~~~~ js 29 | "use strict"; 30 | 31 | const {filter, merge, exist, ArrayIter} = require('obj-filter'); 32 | 33 | const template = { 34 | "runtime": { 35 | "connectionState": undefined, // excluded 36 | "powerState": function (args) {return "HELLO WORLD " + args}, // pass into the checker function 37 | "bootTime": "my boot time", // included 38 | "paused": false, 39 | "snapshotInBackground": 1111111, 40 | "numbers": ArrayIter(filter, Number), // value of "numbers" must be an array, and will check all elements in the array 41 | }, 42 | "running": Boolean 43 | }; 44 | 45 | let clean_data = filter( template, fetchData() ); 46 | let updated_data = filter.merge( clean_data, newUpdates() ); 47 | let clean_full_data = filter.exist( template, fetchData() ); 48 | ~~~~ 49 | 50 | ## Template Object 51 | According to the **Template Object structure**, `obj-filter` supports the following types of value with different behaviors to build the result object. 52 | 53 | ### undefined 54 | If the *value* of the key is `undefined`, the key will be **filtered** (skipped) and will not included in result object. 55 | 56 | ### object 57 | If the *value* of the key is an `object`, `obj-filter` will _dive into it and check the **deeper** level of keys_. 58 | 59 | ### function 60 | If the *value* of the key is an `function`, `obj-filter` will _pass the **value** of the same key in **input data** to the **function**_, and includes it's returned data in result. 61 | So it's your call to customize how you would like to handle, define what you want to do with the input data. Be sure to **return something** from your function. 62 | 63 | - If return `undefined`, the key will be **filtered** (skipped). 64 | - If return anything else, the key will be **included**. 65 | 66 | ### DataTypes / Constructors 67 | `String`, `Number`, `Boolean`, `Array`, `Symbol`, `Map`, `Set`, `WeakMap`, `WeakSet`, `Object`, `Function` in template will do type checking on target object. 68 | 69 | Success if type matches and fails if they don't. 70 | 71 | ### Anything else (string, array, number, etc) 72 | The value of the key will be **included**. 73 | 74 | ### onException callback function 75 | You can pass an additional `onException` callback function into `filter()`, `filter.merge()`, and `filter.exist()` to handle exceptions. 76 | 77 | `onException(template, input, error_msg)` will be called when data expected but type mismatch or undefined. 78 | 79 | ~~~~ js 80 | filter(template, data, (tpl, obj, err) => { console.dir({tpl, obj, err}); return undefined; }); 81 | ~~~~ 82 | 83 | 84 | ## ArrayIter Check Array Elements 85 | 86 | If you want to check `values of array`, use `ArrayIter`. It makes sure that value must be an Array and checks all elements in the array. 87 | 88 | The first two arguments are required: 89 | - (required) filter / merge / exist 90 | - (required) template 91 | - (optional) option 92 | - min: an Integer, default `0`. indicates at least how many elements must be valid. If the result array contains elements fewer than that number, the whole result will be `undefined`. 93 | - onException: a Function, will be called when exception occurred. 94 | 95 | ~~~~ js 96 | const template = { 97 | "a1": ArrayIter(filter, { 98 | "a": String, 99 | "a11": ArrayIter( 100 | exist, 101 | Number, 102 | {"onException": () => undefined} 103 | ), 104 | "a12": ArrayIter(merge, { 105 | "a121": filter.ArrayIter(filter, Number, {"min": 3}) 106 | }), 107 | }), 108 | "a2": ArrayIter(filter, Number) 109 | }; 110 | ~~~~ 111 | 112 | 113 | ## Default Function 114 | 115 | ### Keep only wanted data 116 | 117 | When fetching data through API, sometimes the returned data could be Huge. You need many of them, but there are also too many trash included in returned data. 118 | Copying with `result[xxx] = input[xxx];` each by each, line by line, is a hell. 119 | Now you can copy one returned data structure (in JSON) to your favorite text editor, delete all unwanted lines, paste it back to your code, and use it as template. 120 | 121 | ~~~~ js 122 | "use strict"; 123 | 124 | var filter = require('obj-filter'); 125 | 126 | var template = { 127 | "runtime": { 128 | "connectionState": undefined, // In Template, when the value is undefined, the key will be ignored. 129 | "powerState": function (args) {return "HELLO WORLD " + args}, // pass data into your function, and use it as result value 130 | "bootTime": "my boot time", // The string is just for your own note. Will keep whatever input is in result. 131 | "paused": false, // Will keep whatever input is in result. 132 | "snapshotInBackground": 1111111 // Will keep whatever input is in result. 133 | } 134 | }; 135 | 136 | var data = function_or_somewhere(); 137 | 138 | // Assume: 139 | // var data = { 140 | // "vm": { 141 | // "type": "VirtualMachine" 142 | // }, 143 | // "runtime": { 144 | // "device": 9999, 145 | // "connectionState": "connected", 146 | // "powerState": "poweredOn", 147 | // "bootTime": "2017-04-20T13:56:19.377Z", 148 | // "paused": false, 149 | // "snapshotInBackground": true 150 | // } 151 | //}; 152 | 153 | 154 | var clean_data = filter(template, data); 155 | 156 | // clean_data is: 157 | { 158 | "runtime": { 159 | "powerState": "HELLO WORLD poweredOn", 160 | "bootTime": "2017-04-20T13:56:19.377Z", 161 | "paused": false, 162 | "snapshotInBackground": true 163 | } 164 | }; 165 | ~~~~ 166 | 167 | ### User Data Checks 168 | 169 | Validate user input data in browser (before send to server), or check them at server-side. 170 | 171 | ~~~~ js 172 | var template = { 173 | email: validateEmail(email), // call function validateEmail and use it's return value as value 174 | username: function (username) { 175 | if (/^[a-zA-Z_]+$/.test(username)) { // check if username contains only a-z or underscore 176 | return username; 177 | } else { 178 | throw new Error('Invalid username'); 179 | } 180 | }, 181 | password: "original password" // keep whatever user inputs 182 | } 183 | 184 | save_or_send( filter(template, inputData) ); 185 | ~~~~ 186 | 187 | ### Separated template file 188 | 189 | You can save template into separated files. 190 | 191 | Say _data_template/vmInfo.js_ 192 | 193 | ~~~~ js 194 | { 195 | "runtime": { 196 | "connectionState": undefined, 197 | "powerState": function (args) {return "HELLO WORLD " + args}, 198 | "bootTime": "my boot time", 199 | "paused": false, 200 | "snapshotInBackground": 1111111 201 | } 202 | }; 203 | ~~~~ 204 | 205 | Require it as template 206 | 207 | ~~~~ js 208 | var vm_tpl = require('data_template/vmInfo.js'); 209 | 210 | var vmData = filter(vm_tpl, yourData) 211 | ~~~~ 212 | 213 | ## `merge` Function 214 | 215 | ### Keep template keys when not provided in input data. 216 | 217 | ~~~~ js 218 | "use strict"; 219 | 220 | var filter = require('obj-filter'); 221 | 222 | var template = { 223 | "runtime": { 224 | "connectionState": undefined, 225 | "powerState": function (args) {return "HELLO WORLD " + args}, 226 | "CoffeeTeaOrMe": "Me" 227 | } 228 | }; 229 | 230 | var newUpdates = fetchChanges(); 231 | 232 | // Assume: 233 | // var newUpdates = { 234 | // "runtime": { 235 | // "connectionState": "connected", 236 | // "powerState": "poweredOn" 237 | // } 238 | //}; 239 | 240 | 241 | var updated_data = filter.merge(template, newUpdates); 242 | 243 | // updated_data is: 244 | { 245 | "runtime": { 246 | "powerState": "HELLO WORLD poweredOn", 247 | "bootTime": "2017-04-20T13:56:19.377Z", 248 | "CoffeeTeaOrMe": "Me" 249 | } 250 | }; 251 | ~~~~ 252 | 253 | 254 | ## `exist` Function 255 | 256 | ### Similar to default `filter`, but All Keys in template must also exists in input data. 257 | 258 | ~~~~ js 259 | "use strict"; 260 | 261 | var filter = require('obj-filter'); 262 | 263 | var template = { 264 | "vm": undefined, 265 | "runtime": { 266 | "connectionState": undefined, 267 | "powerState": function (args) {return "HELLO WORLD " + args}, 268 | "bootTime": "my boot time", 269 | "obj jj": { "kk": "yy" } 270 | } 271 | }; 272 | 273 | var data = fetch_from_somewhere(); 274 | 275 | // Assume: 276 | // var data = { 277 | // "runtime": { 278 | // "device": 9999, 279 | // "connectionState": "connected", 280 | // "powerState": "poweredOn", 281 | // "bootTime": 2, 282 | // "obj jj": { "kk": "zz" } 283 | // } 284 | // }; 285 | 286 | 287 | var clean_full_data = filter.exist(template, data); 288 | 289 | // clean_full_data is: 290 | { 291 | "runtime": { 292 | "powerState": "HELLO WORLD poweredOn", 293 | "bootTime": 2, 294 | "obj jj": { "kk": "zz" } 295 | } 296 | }; 297 | ~~~~ 298 | 299 | ## Contribute 300 | 301 | PRs welcome! 302 | If you use/like this module, please don't hesitate to give me a **Star**. I'll be happy whole day! 303 | 304 | _Hope this module can save your time, a tree, and a kitten._ 305 | 306 | 307 | ## License 308 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fbluet%2Fobj-filter.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fbluet%2Fobj-filter?ref=badge_large) 309 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const EXCEPTION_MSGS = { 4 | array: `Doesn't support Array in template yet. The meaning might differ in different context. Please use custom function instead.\nTreating as ${true}`, 5 | mergeTypeChecking: "Using Type Checking in template but object target object doesn't match.\nReturning template Type as result.", 6 | existTypeChecking: "Using Type Checking in template but object target object doesn't match.\nReturning undefined.", 7 | arrayIterFirstArg: "First argument of ArrayIter must be a function of obj-filter", 8 | }; 9 | 10 | const getExceptionMsg = (type, second = false) => `obj-filter: ${second ? second + ": " : ""}${EXCEPTION_MSGS[type]}`; 11 | 12 | function _isType (template) { 13 | if ( 14 | (template === String) 15 | || (template === Number) 16 | || (template === Boolean) 17 | || (template === Array) 18 | || (template === Symbol) 19 | || (template === Map) 20 | || (template === Set) 21 | || (template === WeakMap) 22 | || (template === WeakSet) 23 | || (template === Object) 24 | || (template === Function) 25 | ) { 26 | return true; 27 | } 28 | 29 | return false; 30 | } 31 | 32 | function _sameType (template, obj) { 33 | if ( 34 | (template === String && typeof obj === "string") 35 | || (template === Number && typeof obj === "number") 36 | || (template === Boolean && typeof obj === "boolean") 37 | || (template === Array && Array.isArray(obj)) 38 | || (template === Symbol && obj instanceof Symbol) 39 | || (template === Map && obj instanceof Map) 40 | || (template === Set && obj instanceof Set) 41 | || (template === WeakMap && obj instanceof WeakMap) 42 | || (template === WeakSet && obj instanceof WeakSet) 43 | || (template === Function && typeof obj === "function") 44 | || (template === Object && typeof obj === "object") 45 | ) { 46 | return true; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | 53 | function filter (template, obj, onException) { 54 | 55 | // exclude what's undefined in template 56 | if (typeof template === "undefined") { 57 | return undefined; 58 | } 59 | 60 | // only check Type 61 | if ( _isType(template) ) { 62 | if ( _sameType(template, obj) ) { 63 | return obj; 64 | } 65 | 66 | // type mismatch 67 | if (onException) { 68 | return onException(template, obj); 69 | } 70 | return undefined; 71 | } 72 | 73 | // handle Array as True 74 | if (template instanceof Array) { 75 | if (onException) { 76 | return onException( 77 | template, 78 | obj, 79 | getExceptionMsg("array") 80 | ); 81 | } 82 | return obj; 83 | } 84 | 85 | // filtering 86 | if ( typeof template === "object" ){ 87 | if (typeof obj === "object") { 88 | return Object.keys(template).reduce((result, key) => { 89 | const tmp = filter(template[key], obj[key], onException); 90 | 91 | if (typeof tmp !== "undefined") { 92 | result[key] = tmp; 93 | } 94 | return result; 95 | }, {}); 96 | } 97 | 98 | // type mismatch 99 | if (onException) { 100 | return onException(template, obj); 101 | } 102 | return undefined; 103 | } 104 | 105 | 106 | if ( typeof template === "function" ) { 107 | return template(obj); 108 | } 109 | 110 | return obj; 111 | } 112 | 113 | function merge (template, obj, onException) { 114 | 115 | // exclude what's undefined in template 116 | if (typeof template === "undefined") { 117 | return undefined; 118 | } 119 | 120 | // only check Type 121 | if ( _isType(template) ) { 122 | if ( _sameType(template, obj) ) { 123 | return obj; 124 | } 125 | 126 | // type mismatch 127 | if (onException) { 128 | return onException( 129 | template, 130 | obj, 131 | getExceptionMsg("mergeTypeChecking", "merge") 132 | ); 133 | } 134 | return template; 135 | } 136 | 137 | // handle Array as True 138 | if (template instanceof Array) { 139 | if (onException) { 140 | return onException( 141 | template, 142 | obj, 143 | getExceptionMsg("array", "merge") 144 | ); 145 | } 146 | return obj; 147 | } 148 | 149 | // obj ? obj : template ; 150 | if ( typeof template === "object" ){ 151 | if (typeof obj === "object") { 152 | return Object.keys(template).reduce((result, key) => { 153 | const ret = merge(template[key], obj[key], onException); 154 | 155 | if (typeof ret !== "undefined") { 156 | result[key] = ret; 157 | } else if (typeof template[key] !== "undefined") { 158 | result[key] = template[key]; 159 | } 160 | return result; 161 | }, {}); 162 | } else { 163 | // type mismatch 164 | if (onException) { 165 | return onException( 166 | template, 167 | obj, 168 | getExceptionMsg("mergeTypeChecking", "merge") 169 | ); 170 | } 171 | return template; 172 | } 173 | } 174 | 175 | // must before "undefined" handling, so user can handle undefined if they wanted 176 | if ( typeof template === "function" ) { 177 | return template(obj); 178 | } 179 | 180 | // must after typeof template === "function", so user can handle it if they wanted 181 | if (typeof obj === "undefined") { 182 | return template; 183 | } 184 | 185 | return obj; 186 | } 187 | 188 | function exist (template, obj, onException) { 189 | 190 | // exclude what's undefined in template 191 | if (typeof template === "undefined") { 192 | return undefined; 193 | } 194 | 195 | // only check Type 196 | if ( _isType(template) ) { 197 | if ( _sameType(template, obj) ) { 198 | return obj; 199 | } 200 | 201 | // type mismatch 202 | if (onException) { 203 | return onException( 204 | template, 205 | obj, 206 | getExceptionMsg("existTypeChecking") 207 | ); 208 | } 209 | 210 | return undefined; 211 | } 212 | 213 | // handle Array as True 214 | if (template instanceof Array) { 215 | if (onException) { 216 | return onException( 217 | template, 218 | obj, 219 | getExceptionMsg("array", "exist") 220 | ); 221 | } 222 | return obj; 223 | } 224 | 225 | // must before "undefined" handling, so user can handle undefined if they wanted 226 | if (typeof template === "function") { 227 | return template(obj); 228 | } 229 | 230 | // must after typeof template === "function", so user can handle it if they wanted 231 | if (typeof obj === "undefined") { 232 | return undefined; 233 | } 234 | 235 | // check if all keys exists recursively 236 | if (typeof template === "object") { 237 | return Object.keys(template).reduce((result, key) => { 238 | if (template[key] === undefined) { 239 | // value "undefined" means skip 240 | return result; 241 | } 242 | 243 | const tmp = exist(template[key], obj[key], onException); 244 | 245 | if (typeof tmp === "undefined") { 246 | return undefined; 247 | } 248 | 249 | result[key] = tmp; 250 | 251 | return result; 252 | }, {}); 253 | } 254 | 255 | // return whatever obj has 256 | return obj; 257 | } 258 | 259 | 260 | function ArrayIter (checker, template, {min = 0, onException} = {}) { 261 | 262 | if (typeof(checker) !== "function") { 263 | if (onException) { 264 | return onException(undefined, undefined, getExceptionMsg("arrayIterFirstArg")); 265 | } 266 | 267 | throw new Error(getExceptionMsg("arrayIterFirstArg")); 268 | } 269 | 270 | return function (array) { 271 | if (!(array instanceof Array)) { 272 | return undefined; 273 | } 274 | 275 | const result = array.map((value) => checker(template, value, onException)) 276 | .filter((x) => x !== undefined); 277 | 278 | return result.length >= min ? result : undefined; 279 | }; 280 | } 281 | 282 | module.exports = filter; 283 | module.exports.filter = filter; 284 | module.exports.merge = merge; 285 | module.exports.exist = exist; 286 | module.exports.ArrayIter = ArrayIter; 287 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obj-filter", 3 | "version": "2.3.8", 4 | "description": "JavaScript Object Filter. Deep filtering key/content recursively. Support type checking, wildcard, nested, and filter function in template.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "nyc --check-coverage --reporter=lcov tape ./t/[0-9]*.test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/BlueT/obj-filter.git" 12 | }, 13 | "keywords": [ 14 | "object", 15 | "filter", 16 | "merge", 17 | "exist", 18 | "template", 19 | "javascript", 20 | "nodejs", 21 | "wildcard", 22 | "nested", 23 | "function", 24 | "browser", 25 | "type", 26 | "checking" 27 | ], 28 | "author": "BlueT - Matthew Lien - 練喆明", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/BlueT/obj-filter/issues" 32 | }, 33 | "homepage": "https://github.com/BlueT/obj-filter#readme", 34 | "devDependencies": { 35 | "app-root-path": "^3.1.0", 36 | "nyc": "^17.1.0", 37 | "tape": "^5.6.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # must be unique in a given SonarQube instance 2 | sonar.projectKey=obj-filter 3 | # this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1. 4 | sonar.projectName=obj-filter 5 | #sonar.projectVersion=2.3.4 6 | 7 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 8 | # This property is optional if sonar.modules is set. 9 | sonar.sources=. 10 | #~ sonar.exclusions=t/**, node_modules/**, coverage/** 11 | sonar.exclusions=node_modules/**, coverage/**, t/** 12 | 13 | # Encoding of the source code. Default is default system encoding 14 | sonar.sourceEncoding=UTF-8 15 | 16 | # LCOV coverage file from istanbul and tape 17 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 18 | 19 | sonar.organization=bluet-github 20 | -------------------------------------------------------------------------------- /t/1-filter.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var test = require("tape"); // assign the tape library to the variable "test" 3 | var appRoot = require("app-root-path"); 4 | var filter = require(appRoot + "/index.js"); 5 | 6 | var template = { 7 | "runtime": { 8 | "connectionState": undefined, 9 | "powerState": function (args) {return "HELLO WORLD " + args;}, 10 | "bootTime": ["my boot time"], 11 | "paused": false, 12 | "snapshotInBackground": 1111111, 13 | "CoffeeTeaOrMe": "Me", 14 | "obj jj": { "kk": "yy" } 15 | }, 16 | "running": Boolean 17 | }; 18 | 19 | var data = { 20 | "vm": { 21 | "type": "VirtualMachine" 22 | }, 23 | "runtime": { 24 | "device": 9999, 25 | "connectionState": "connected", 26 | "powerState": "poweredOn", 27 | "bootTime": undefined, 28 | "paused": false, 29 | "snapshotInBackground": false 30 | } 31 | }; 32 | 33 | var exam_filter = { 34 | "runtime": { 35 | "powerState": "HELLO WORLD poweredOn", 36 | "paused": false, 37 | "snapshotInBackground": false 38 | } 39 | }; 40 | 41 | 42 | test("filter with empty template should fail", function (t) { 43 | t.false(filter(undefined, data), "undefined template results empty result"); 44 | t.deepEqual(filter({}, data), {}, "undefined template results empty result"); 45 | t.end(); 46 | }); 47 | 48 | test("filter with String Type template", function (t) { 49 | t.equal("data", filter(String, "data"), "string matches String"); 50 | t.false(filter(String, String), "not equal to `String` object"); 51 | t.equal("data", filter(String, String, () => {return "data";} ), "use onException to help"); 52 | t.end(); 53 | }); 54 | 55 | test("filter should contain: runtime, powerState, bootTime, paused, snapshotInBackground", function (t) { 56 | var result_filter = filter(template, data); 57 | t.deepEqual(result_filter, exam_filter); 58 | t.deepEqual(exam_filter, filter(template, data, () => {return undefined;} )); 59 | t.end(); 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /t/2-exist.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var test = require("tape"); // assign the tape library to the variable "test" 3 | var appRoot = require("app-root-path"); 4 | var filter = require(appRoot + "/index.js"); 5 | 6 | var template = { 7 | "vm": undefined, 8 | "runtime": { 9 | "connectionState": undefined, 10 | "powerState": function (args) {return "HELLO WORLD " + args;}, 11 | "bootTime": ["my boot time"], 12 | "obj jj": { "kk": "yy" } 13 | }, 14 | "running": Boolean 15 | }; 16 | 17 | var data_success = { 18 | "runtime": { 19 | "device": 9999, 20 | "connectionState": "connected", 21 | "powerState": "poweredOn", 22 | "bootTime": 2, 23 | "obj jj": { "kk": "zz" } 24 | }, 25 | "running": false 26 | }; 27 | 28 | var data_fail = { 29 | "runtime": { 30 | "device": 9999, 31 | "connectionState": "connected", 32 | "powerState": "poweredOn", 33 | "bootTime": undefined, 34 | "paused": false, 35 | "snapshotInBackground": false 36 | } 37 | }; 38 | 39 | var exam_success = { 40 | "runtime": { 41 | "powerState": "HELLO WORLD poweredOn", 42 | "bootTime": 2, 43 | "obj jj": { "kk": "zz" } 44 | }, 45 | "running": false 46 | }; 47 | 48 | 49 | test("filter.exist with undefined template should fail", function (t) { 50 | var result_fail = filter.exist(undefined, data_success); 51 | t.false(result_fail); 52 | t.end(); 53 | }); 54 | 55 | test("filter.exist with String Type template", function (t) { 56 | t.equal("data", filter.exist(String, "data"), "string matches String"); 57 | t.false(filter.exist(String, String), "not equal to `String` object"); 58 | t.equal("cb", filter.exist(String, 1, () => { return "cb"; })); 59 | t.end(); 60 | }); 61 | 62 | test("filter.exist should contain: runtime, connectionState, powerState, bootTime, obj jj, kk", function (t) { 63 | var result = filter.exist(template, data_success); 64 | t.deepEqual(result, exam_success); 65 | t.end(); 66 | }); 67 | 68 | test("filter.exist should fail", function (t) { 69 | var result_fail = filter.exist(template, data_fail); 70 | t.false(result_fail); 71 | t.false(filter.exist(template, undefined)); 72 | t.false(filter.exist(template, data_fail, () => {return undefined;} )); 73 | t.end(); 74 | }); -------------------------------------------------------------------------------- /t/2-merge.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var test = require("tape"); // assign the tape library to the variable "test" 3 | var appRoot = require("app-root-path"); 4 | var filter = require(appRoot + "/index.js"); 5 | 6 | var template = { 7 | "runtime": { 8 | "connectionState": undefined, 9 | "powerState": function (args) {return "HELLO WORLD " + args;}, 10 | "bootTime": ["my boot time"], 11 | "paused": false, 12 | "snapshotInBackground": 1111111, 13 | "CoffeeTeaOrMe": "Me", 14 | "obj jj": { "kk": "yy" } 15 | }, 16 | "running": Boolean 17 | }; 18 | 19 | var data = { 20 | "vm": { 21 | "type": "VirtualMachine" 22 | }, 23 | "runtime": { 24 | "device": 9999, 25 | "connectionState": "connected", 26 | "powerState": "poweredOn", 27 | "bootTime": undefined, 28 | "paused": false, 29 | "snapshotInBackground": false 30 | } 31 | }; 32 | 33 | 34 | var exam_merge = { 35 | "runtime": { 36 | "powerState": "HELLO WORLD poweredOn", 37 | "bootTime": ["my boot time"], 38 | "paused": false, 39 | "snapshotInBackground": false, 40 | "CoffeeTeaOrMe": "Me", 41 | "obj jj": { "kk": "yy" } 42 | }, 43 | "running": Boolean 44 | }; 45 | 46 | test("filter.merge with undefined template should fail", function (t) { 47 | var result_fail = filter.merge(undefined, data, () => {return undefined;} ); 48 | t.false(result_fail, "empty template results empty result"); 49 | t.end(); 50 | }); 51 | 52 | test("filter.merge with String Type template", function (t) { 53 | t.equal("data", filter.merge(String, "data"), "string matches String"); 54 | t.equal(String, filter.merge(String, String), "not equal to `String` object"); 55 | t.end(); 56 | }); 57 | 58 | test("filter.merge should contain: runtime, powerState, bootTime, paused, snapshotInBackground, CoffeeTeaOrMe", function (t) { 59 | var result_merge = filter.merge(template, data); 60 | t.deepEqual(result_merge, exam_merge); 61 | t.deepEqual(exam_merge, filter.merge(template, data, () => {return undefined;} )); 62 | t.end(); 63 | }); 64 | -------------------------------------------------------------------------------- /t/3-ArrayIter.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const test = require("tape"); // assign the tape library to the variable "test" 3 | const appRoot = require("app-root-path"); 4 | const {filter, merge, exist, ArrayIter} = require(appRoot + "/index.js"); 5 | 6 | const template = { 7 | "a1": ArrayIter(filter, { 8 | "a": String, 9 | "a11": ArrayIter(exist, Number, {"min": 3}), 10 | "a12": ArrayIter( 11 | merge, 12 | { 13 | "a121": filter.ArrayIter(filter, Number) 14 | } 15 | ), 16 | "b": ArrayIter(exist, Number), 17 | }), 18 | "a2": ArrayIter(filter, Number, {"onException": () => undefined}) 19 | }; 20 | 21 | var data = { 22 | "a1": [ 23 | { 24 | "a": "a", 25 | "a11": [1, 2, 3, {}], 26 | "a12": [ 27 | { 28 | "a121": [1, 2, 3] 29 | }, 30 | { 31 | "a121": [1, 2, 3] 32 | }, 33 | { 34 | "a121": [1, 2, 3] 35 | }, 36 | ], 37 | "b": "b" 38 | }, 39 | { 40 | "a11": [1, 2, 3], 41 | "a12": [ 42 | { 43 | "a121": [1, 2, 3] 44 | }, 45 | { 46 | "a121": [1, 2, 3] 47 | }, 48 | { 49 | "a121": [1, 2, 3] 50 | } 51 | ], 52 | }, 53 | ], 54 | "a2": [1, 2, 3] 55 | }; 56 | 57 | const exam_data = { 58 | "a1": [ 59 | { 60 | "a": "a", 61 | "a11": [1, 2, 3], 62 | "a12": [ 63 | { 64 | "a121": [1, 2, 3] 65 | }, 66 | { 67 | "a121": [1, 2, 3] 68 | }, 69 | { 70 | "a121": [1, 2, 3] 71 | } 72 | ], 73 | }, 74 | { 75 | "a11": [1, 2, 3], 76 | "a12": [ 77 | { 78 | "a121": [1, 2, 3] 79 | }, 80 | { 81 | "a121": [1, 2, 3] 82 | }, 83 | { 84 | "a121": [1, 2, 3] 85 | } 86 | ], 87 | }, 88 | ], 89 | "a2": [1, 2, 3] 90 | }; 91 | 92 | 93 | test("filter with ArrayIter and undefined filter should fail", function (t) { 94 | let result = filter( 95 | {"a2": ArrayIter(undefined, Number, {"onException": () => undefined})}, 96 | {"a2": [1, 2, 3]} 97 | ); 98 | t.deepEqual(result, {}, "undefined filter with customer onException results empty result"); 99 | 100 | t.throws(() => filter( 101 | {"a2": ArrayIter(undefined, Number)}, 102 | {"a2": [1, 2, 3]} 103 | ), Error, "undefined filter with default onException throws Error"); 104 | 105 | t.end(); 106 | }); 107 | 108 | test("filter with ArrayIter", function (t) { 109 | let result_filter = filter(template, data); 110 | t.deepEqual(result_filter, exam_data); 111 | t.deepEqual(exam_data, filter(template, data, () => {return undefined;} )); 112 | t.end(); 113 | }); 114 | 115 | --------------------------------------------------------------------------------