├── logo.png ├── test ├── fixture │ ├── invalid.json │ └── activities.json ├── browser │ ├── jsonMask.html │ ├── jsonMaskMin.html │ ├── test.js │ └── index.html ├── filter-test.js ├── compiler-test.js ├── cli-test.js └── index-test.js ├── .gitignore ├── lib ├── index.js ├── util.js ├── filter.js └── compiler.js ├── example ├── simple_server.js └── server.js ├── .github └── workflows │ └── node.js.yml ├── LICENSE ├── package.json ├── bin └── json-mask.js └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemtsov/json-mask/HEAD/logo.png -------------------------------------------------------------------------------- /test/fixture/invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "this-line-has-a-wrong-comma": true, 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules 4 | coverage 5 | .vscode 6 | package-lock.json 7 | 8 | -------------------------------------------------------------------------------- /test/browser/jsonMask.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Check the console for errors... 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var compile = require('./compiler') 2 | var filter = require('./filter') 3 | 4 | function mask (obj, mask) { 5 | return filter(obj, compile(mask)) || null 6 | } 7 | 8 | mask.compile = compile 9 | mask.filter = filter 10 | 11 | module.exports = mask 12 | -------------------------------------------------------------------------------- /test/browser/jsonMaskMin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Check the console for errors... 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/browser/test.js: -------------------------------------------------------------------------------- 1 | /* global jsonMask */ 2 | 3 | function assert (o) { if (!o) throw new Error('AssertionError') } 4 | var r = jsonMask({ p: { a: 1, b: 2 }, z: 1 }, 'p/a,z') 5 | assert(r.p.a) 6 | assert(r.z) 7 | assert(typeof r.p.b === 'undefined') 8 | document.getElementById('res').innerHTML = 'ok' 9 | -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
jsonMask
6 |
7 | 8 |
jsonMaskMin
9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/simple_server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var url = require('url') 3 | var mask = require('../lib') 4 | var server 5 | 6 | server = http.createServer(function (req, res) { 7 | var fields = url.parse(req.url, true).query.fields 8 | var data = { 9 | firstName: 'Mohandas', 10 | lastName: 'Gandhi', 11 | aliases: [{ 12 | firstName: 'Mahatma', 13 | lastName: 'Gandhi' 14 | }, { 15 | firstName: 'Bapu' 16 | }] 17 | } 18 | res.writeHead(200, { 'Content-Type': 'application/json' }) 19 | res.end(JSON.stringify(mask(data, fields))) 20 | }) 21 | 22 | server.listen(4000) 23 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var ObjProto = Object.prototype 2 | 3 | exports.isEmpty = isEmpty 4 | exports.isArray = Array.isArray || isArray 5 | exports.isObject = isObject 6 | exports.has = has 7 | 8 | function isEmpty (obj) { 9 | if (obj == null) return true 10 | if (isArray(obj) || 11 | (typeof obj === 'string')) return (obj.length === 0) 12 | for (var key in obj) if (has(obj, key)) return false 13 | return true 14 | } 15 | 16 | function isArray (obj) { 17 | return ObjProto.toString.call(obj) === '[object Array]' 18 | } 19 | 20 | function isObject (obj) { 21 | return (typeof obj === 'function') || (typeof obj === 'object' && !!obj) 22 | } 23 | 24 | function has (obj, key) { 25 | return ObjProto.hasOwnProperty.call(obj, key) 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Yuriy Nemtsov 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var url = require('url') 3 | var mask = require('../lib') 4 | var activities = require('../test/fixture/activities.json') 5 | var personalActivities = require('../test/fixture/personal_activities.json') 6 | var server 7 | 8 | /** 9 | * Using json-mask to implement the Google API Partial Responses 10 | * https://developers.google.com/+/api/#partial-responses 11 | */ 12 | 13 | server = http.createServer(function (req, res) { 14 | var parsedUrl = url.parse(req.url, true) 15 | var data = /^\/personal/.test(parsedUrl.pathname) ? personalActivities : activities 16 | var query = parsedUrl.query 17 | res.writeHead(200, { 'Content-Type': 'application/json' }) 18 | res.end(JSON.stringify(mask(data, query.fields), true, 2)) 19 | }) 20 | 21 | server.listen(4000, function () { 22 | var prefix = 'curl \'http://localhost:4000%s?fields=%s\'' 23 | console.log('Server running on :4000, try the following:') 24 | console.log(prefix, '/', 'title') 25 | console.log(prefix, '/', 'kind,updated') 26 | console.log(prefix, '/', 'url,object(content,attachments/url)') 27 | console.log(prefix, '/personal', 'items/object/*') 28 | console.log(prefix, '/personal', 'items/object/*/totalItems') 29 | }) 30 | -------------------------------------------------------------------------------- /test/filter-test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | var filter = require('../lib/filter') 4 | var assert = require('assert') 5 | var compiledMask 6 | var object 7 | var expected 8 | 9 | // a,b(d/*/z,b(g)),c 10 | compiledMask = { 11 | a: { type: 'object' }, 12 | b: { 13 | type: 'array', 14 | properties: { 15 | d: { 16 | type: 'object', 17 | properties: { 18 | '*': { 19 | type: 'object', 20 | isWildcard: true, 21 | properties: { 22 | z: { type: 'object' } 23 | } 24 | } 25 | } 26 | }, 27 | b: { 28 | type: 'array', 29 | properties: { 30 | g: { type: 'object' } 31 | } 32 | } 33 | } 34 | }, 35 | c: { type: 'object' }, 36 | 'd/e': { type: 'object' }, 37 | '*': { type: 'object' } 38 | } 39 | 40 | object = { 41 | a: 11, 42 | n: 0, 43 | b: [{ 44 | d: { g: { z: 22 }, b: 34, c: { a: 32 } }, 45 | b: [{ z: 33 }], 46 | k: 99 47 | }], 48 | c: 44, 49 | g: 99, 50 | 'd/e': 101, 51 | '*': 110 52 | } 53 | 54 | expected = { 55 | a: 11, 56 | b: [{ 57 | d: { 58 | g: { 59 | z: 22 60 | }, 61 | c: {} 62 | }, 63 | b: [{}] 64 | }], 65 | c: 44, 66 | 'd/e': 101, 67 | '*': 110 68 | } 69 | 70 | describe('filter', function () { 71 | it('should filter object for a compiled mask', function () { 72 | assert.deepStrictEqual(filter(object, compiledMask), expected) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-mask", 3 | "version": "2.0.0", 4 | "description": "Tiny language and engine for selecting specific parts of a JS object, hiding the rest.", 5 | "main": "lib/index", 6 | "files": [ 7 | "bin", 8 | "build", 9 | "lib" 10 | ], 11 | "scripts": { 12 | "test": "npm run lint && mocha", 13 | "test:all": "nve --parallel 14,16,18 mocha", 14 | "test-watch": "mocha -w -G -R min", 15 | "test-cov": "nyc --reporter=html --reporter=text mocha", 16 | "lint": "standard 'lib/**/*.js' 'test/**/*.js' 'bin/**/*.js'", 17 | "build-browser": "npm run-script _build-browser-full; npm run-script _build-browser-license; npm run-script _build-browser-min", 18 | "_build-browser-full": "browserify -s jsonMask -e lib/index.js | sed -e \"s/\\[ *'.*' *\\]/;/\" > build/jsonMask.js", 19 | "_build-browser-license": "cat build/copyright | cat - build/jsonMask.js | tee build/jsonMask.js", 20 | "_build-browser-min": "cat build/jsonMask.js | uglifyjs --comments > build/jsonMask.min.js" 21 | }, 22 | "bin": "bin/json-mask.js", 23 | "engines": { 24 | "node": ">=14.0.0" 25 | }, 26 | "keywords": [ 27 | "mask", 28 | "filter", 29 | "select", 30 | "fields", 31 | "projection", 32 | "query", 33 | "json", 34 | "cli" 35 | ], 36 | "author": "nemtsov@gmail.com", 37 | "license": "MIT", 38 | "devDependencies": { 39 | "browserify": "^17.0.0", 40 | "mocha": "^10.0.0", 41 | "nve": "^14.0.0", 42 | "nyc": "^15.1.0", 43 | "standard": "^15.0.1", 44 | "uglify-js": "^3.15.4" 45 | }, 46 | "eslintConfig": { 47 | "rules": { 48 | "no-var": "off" 49 | } 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "git://github.com/nemtsov/json-mask.git" 54 | }, 55 | "dependencies": {} 56 | } 57 | -------------------------------------------------------------------------------- /bin/json-mask.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const mask = require('../lib') 3 | const fs = require('fs') 4 | const { promisify } = require('util') 5 | 6 | const readFile = promisify(fs.readFile) 7 | const missingInput = () => new Error('Either pipe input into json-mask or specify a file as second argument') 8 | 9 | function usage (error) { 10 | if (error) console.error(error.message) 11 | console.log('Usage: json-mask [input.json]') 12 | console.log('Examples:') 13 | console.log(' json-mask "url,object(content,attachments/url)" input.json') 14 | console.log(' cat input.json | json-mask "url,object(content,attachments/url)"') 15 | console.log(' curl https://api.myjson.com/bins/krrxw | json-mask "url,object(content,attachments/url)"') 16 | process.exit(1) 17 | } 18 | 19 | function pipeInput () { 20 | return new Promise((resolve, reject) => { 21 | process.stdin.resume() 22 | process.stdin.setEncoding('utf8') 23 | 24 | let data = '' 25 | process.stdin.on('data', chunk => (data += chunk)) 26 | process.stdin.on('end', () => data ? resolve(data) : reject(missingInput())) 27 | 28 | process.stdin.on('error', reject) 29 | }) 30 | } 31 | 32 | function getInput (inputFilePath) { 33 | if (inputFilePath) return readFile(inputFilePath, { encoding: 'UTF-8' }) 34 | if (!process.stdin.isTTY) return pipeInput() 35 | return Promise.reject(missingInput()) 36 | } 37 | 38 | /** 39 | * Runs the command line filter 40 | * 41 | * @param {String} fields to mask 42 | * @param {String} inputFilePath absolute or relative path for input file to read 43 | */ 44 | async function run (fields, inputFilePath) { 45 | if (!fields) throw new Error('Fields argument missing') 46 | const input = await getInput(inputFilePath) 47 | const json = JSON.parse(input) 48 | const masked = mask(json, fields) 49 | console.log(JSON.stringify(masked)) 50 | } 51 | 52 | run(process.argv[2], process.argv[3]) 53 | .catch(usage) 54 | -------------------------------------------------------------------------------- /lib/filter.js: -------------------------------------------------------------------------------- 1 | var util = require('./util') 2 | 3 | module.exports = filter 4 | 5 | function filter (obj, compiledMask) { 6 | return util.isArray(obj) 7 | ? _arrayProperties(obj, compiledMask) 8 | : _properties(obj, compiledMask) 9 | } 10 | 11 | // wrap array & mask in a temp object; 12 | // extract results from temp at the end 13 | function _arrayProperties (arr, mask) { 14 | var obj = _properties({ _: arr }, { 15 | _: { 16 | type: 'array', 17 | properties: mask 18 | } 19 | }) 20 | return obj && obj._ 21 | } 22 | 23 | function _properties (obj, mask) { 24 | var maskedObj, key, value, ret, retKey, typeFunc 25 | if (!obj || !mask) return obj 26 | 27 | if (util.isArray(obj)) maskedObj = [] 28 | else if (util.isObject(obj)) maskedObj = {} 29 | 30 | for (key in mask) { 31 | if (!util.has(mask, key)) continue 32 | value = mask[key] 33 | ret = undefined 34 | typeFunc = (value.type === 'object') ? _object : _array 35 | if (value.isWildcard) { 36 | ret = _forAll(obj, value.properties, typeFunc) 37 | for (retKey in ret) { 38 | if (!util.has(ret, retKey)) continue 39 | maskedObj[retKey] = ret[retKey] 40 | } 41 | } else { 42 | ret = typeFunc(obj, key, value.properties) 43 | if (typeof ret !== 'undefined') maskedObj[key] = ret 44 | } 45 | } 46 | return maskedObj 47 | } 48 | 49 | function _forAll (obj, mask, fn) { 50 | var ret = {} 51 | var key 52 | var value 53 | for (key in obj) { 54 | if (!util.has(obj, key)) continue 55 | value = fn(obj, key, mask) 56 | if (typeof value !== 'undefined') ret[key] = value 57 | } 58 | return ret 59 | } 60 | 61 | function _object (obj, key, mask) { 62 | var value = obj[key] 63 | if (util.isArray(value)) return _array(obj, key, mask) 64 | return mask ? _properties(value, mask) : value 65 | } 66 | 67 | function _array (object, key, mask) { 68 | var ret = [] 69 | var arr = object[key] 70 | var obj 71 | var maskedObj 72 | var i 73 | var l 74 | if (!util.isArray(arr)) return _properties(arr, mask) 75 | if (util.isEmpty(arr)) return arr 76 | for (i = 0, l = arr.length; i < l; i++) { 77 | obj = arr[i] 78 | maskedObj = _properties(obj, mask) 79 | if (typeof maskedObj !== 'undefined') ret.push(maskedObj) 80 | } 81 | return ret.length ? ret : undefined 82 | } 83 | -------------------------------------------------------------------------------- /test/fixture/activities.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "plus#activity", 3 | "etag": "\"DOKFJGXi7L9ogpHc3dzouWOBEEg/ZiaatWNPRL3cQ-I-WbeQPR_yVa0\"", 4 | "title": "Congratulations! You have successfully fetched an explicit public activity. The attached video is your...", 5 | "published": "2011-09-08T21:17:41.232Z", 6 | "updated": "2011-10-04T17:25:26.000Z", 7 | "id": "z12gtjhq3qn2xxl2o224exwiqruvtda0i", 8 | "url": "https://plus.google.com/102817283354809142195/posts/F97fqZwJESL", 9 | "actor": { 10 | "id": "102817283354809142195", 11 | "displayName": "Jenny Murphy", 12 | "url": "https://plus.google.com/102817283354809142195", 13 | "image": { 14 | "url": "https://lh4.googleusercontent.com/-yth5HLY4Qi4/AAAAAAAAAAI/AAAAAAAAPVs/fAq4PVOVBdc/photo.jpg?sz=50" 15 | } 16 | }, 17 | "verb": "post", 18 | "object": { 19 | "objectType": "note", 20 | "content": "Congratulations! You have successfully fetched an explicit public activity. The attached video is your reward. :)", 21 | "url": "https://plus.google.com/102817283354809142195/posts/F97fqZwJESL", 22 | "replies": { 23 | "totalItems": 16, 24 | "selfLink": "https://www.googleapis.com/plus/v1/activities/z12gtjhq3qn2xxl2o224exwiqruvtda0i/comments" 25 | }, 26 | "plusoners": { 27 | "totalItems": 44, 28 | "selfLink": "https://www.googleapis.com/plus/v1/activities/z12gtjhq3qn2xxl2o224exwiqruvtda0i/people/plusoners" 29 | }, 30 | "resharers": { 31 | "totalItems": 1, 32 | "selfLink": "https://www.googleapis.com/plus/v1/activities/z12gtjhq3qn2xxl2o224exwiqruvtda0i/people/resharers" 33 | }, 34 | "attachments": [{ 35 | "objectType": "video", 36 | "displayName": "Rick Astley - Never Gonna Give You Up", 37 | "content": "Music video by Rick Astley performing Never Gonna Give You Up. YouTube view counts pre-VEVO: 2,573,462 (C) 1987 PWL", 38 | "url": "http://www.youtube.com/watch?v=dQw4w9WgXcQ", 39 | "image": { 40 | "url": "https://lh3.googleusercontent.com/proxy/ex1bQ9_TpVClePgZxFmCPVxYeJUHW5dixt53FLmup-q44pd1mwO6rPIPti6tDWbjitBclMm5Ou595xPEMKq2b8Qu3mQ_TzX0kOqksE8o1w=w506-h284-n", 41 | "type": "image/jpeg", 42 | "height": 284, 43 | "width": 506 44 | }, 45 | "embed": { 46 | "url": "http://www.youtube.com/v/dQw4w9WgXcQ&hl=en&fs=1&autoplay=1", 47 | "type": "application/x-shockwave-flash" 48 | } 49 | }] 50 | }, 51 | "provider": { 52 | "title": "Google+" 53 | }, 54 | "access": { 55 | "kind": "plus#acl", 56 | "description": "Public", 57 | "items": [{ 58 | "type": "public" 59 | }] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | var util = require('./util') 2 | var TERMINALS = { ',': 1, '/': 2, '(': 3, ')': 4 } 3 | var ESCAPE_CHAR = '\\' 4 | var WILDCARD_CHAR = '*' 5 | 6 | module.exports = compile 7 | 8 | /** 9 | * Compiler 10 | * 11 | * Grammar: 12 | * Props ::= Prop | Prop "," Props 13 | * Prop ::= Object | Array 14 | * Object ::= NAME | NAME "/" Prop 15 | * Array ::= NAME "(" Props ")" 16 | * NAME ::= ? all visible characters except "\" ? | EscapeSeq | Wildcard 17 | * Wildcard ::= "*" 18 | * EscapeSeq ::= "\" ? all visible characters ? 19 | * 20 | * Examples: 21 | * a 22 | * a,d,g 23 | * a/b/c 24 | * a(b) 25 | * ob,a(k,z(f,g/d)),d 26 | * a\/b/c 27 | */ 28 | 29 | function compile (text) { 30 | if (!text) return null 31 | return parse(scan(text)) 32 | } 33 | 34 | function scan (text) { 35 | var i = 0 36 | var len = text.length 37 | var tokens = [] 38 | var name = '' 39 | var ch 40 | 41 | function maybePushName () { 42 | if (!name) return 43 | tokens.push({ tag: '_n', value: name }) 44 | name = '' 45 | } 46 | 47 | for (; i < len; i++) { 48 | ch = text.charAt(i) 49 | if (ch === ESCAPE_CHAR) { 50 | i++ 51 | if (i >= len) { 52 | name += ESCAPE_CHAR 53 | break 54 | } 55 | ch = text.charAt(i) 56 | name += ch === WILDCARD_CHAR ? ESCAPE_CHAR + WILDCARD_CHAR : ch 57 | } else if (TERMINALS[ch]) { 58 | maybePushName() 59 | tokens.push({ tag: ch }) 60 | } else { 61 | name += ch 62 | } 63 | } 64 | maybePushName() 65 | 66 | return tokens 67 | } 68 | 69 | function parse (tokens) { 70 | return _buildTree(tokens, {}) 71 | } 72 | 73 | function _buildTree (tokens, parent) { 74 | var props = {} 75 | var token 76 | 77 | while ((token = tokens.shift())) { 78 | if (token.tag === '_n') { 79 | token.type = 'object' 80 | token.properties = _buildTree(tokens, token) 81 | if (parent.hasChild) { 82 | _addToken(token, props) 83 | return props 84 | } 85 | } else if (token.tag === ',') { 86 | return props 87 | } else if (token.tag === '(') { 88 | parent.type = 'array' 89 | continue 90 | } else if (token.tag === ')') { 91 | return props 92 | } else if (token.tag === '/') { 93 | parent.hasChild = true 94 | continue 95 | } 96 | _addToken(token, props) 97 | } 98 | 99 | return props 100 | } 101 | 102 | function _addToken (token, props) { 103 | var prop = { type: token.type } 104 | 105 | if (token.value === WILDCARD_CHAR) prop.isWildcard = true 106 | else if (token.value === ESCAPE_CHAR + WILDCARD_CHAR) token.value = WILDCARD_CHAR 107 | 108 | if (!util.isEmpty(token.properties)) { 109 | prop.properties = token.properties 110 | } 111 | 112 | props[token.value] = prop 113 | } 114 | -------------------------------------------------------------------------------- /test/compiler-test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | var compile = require('../lib/compiler') 4 | var assert = require('assert') 5 | var util = require('../lib/util') 6 | var tests 7 | 8 | tests = { 9 | a: { a: { type: 'object' } }, 10 | 'a,b,c': { 11 | a: { type: 'object' }, 12 | b: { type: 'object' }, 13 | c: { type: 'object' } 14 | }, 15 | 'a/*/c': { 16 | a: { 17 | type: 'object', 18 | properties: { 19 | '*': { 20 | type: 'object', 21 | isWildcard: true, 22 | properties: { 23 | c: { type: 'object' } 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | 'a,b(d/*/g,b),c': { 30 | a: { type: 'object' }, 31 | b: { 32 | type: 'array', 33 | properties: { 34 | d: { 35 | type: 'object', 36 | properties: { 37 | '*': { 38 | type: 'object', 39 | isWildcard: true, 40 | properties: { 41 | g: { type: 'object' } 42 | } 43 | } 44 | } 45 | }, 46 | b: { type: 'object' } 47 | } 48 | }, 49 | c: { type: 'object' } 50 | }, 51 | 'a(b/c,e)': { 52 | a: { 53 | type: 'array', 54 | properties: { 55 | b: { 56 | type: 'object', 57 | properties: { 58 | c: { type: 'object' } 59 | } 60 | }, 61 | e: { type: 'object' } 62 | } 63 | } 64 | }, 65 | 'a(b/c),e': { 66 | a: { 67 | type: 'array', 68 | properties: { 69 | b: { 70 | type: 'object', 71 | properties: { 72 | c: { type: 'object' } 73 | } 74 | } 75 | } 76 | }, 77 | e: { type: 'object' } 78 | }, 79 | 'a(b/c/d),e': { 80 | a: { 81 | type: 'array', 82 | properties: { 83 | b: { 84 | type: 'object', 85 | properties: { 86 | c: { 87 | type: 'object', 88 | properties: { 89 | d: { type: 'object' } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | }, 96 | e: { type: 'object' } 97 | }, 98 | 'a(b/g(c)),e': { 99 | a: { 100 | type: 'array', 101 | properties: { 102 | b: { 103 | type: 'object', 104 | properties: { 105 | g: { 106 | type: 'array', 107 | properties: { 108 | c: { 109 | type: 'object' 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | }, 117 | e: { type: 'object' } 118 | }, 119 | 'a\\/b\\/c': { 120 | 'a/b/c': { 121 | type: 'object' 122 | } 123 | }, 124 | 'a\\(b\\)c': { 125 | 'a(b)c': { 126 | type: 'object' 127 | } 128 | }, 129 | // escaped b (`\b`) in our language resolves to `b` character. 130 | 'a\\bc': { 131 | abc: { 132 | type: 'object' 133 | } 134 | }, 135 | '\\*': { 136 | '*': { 137 | type: 'object' 138 | } 139 | }, 140 | '*': { 141 | '*': { 142 | type: 'object', 143 | isWildcard: true 144 | } 145 | }, 146 | '*(a,b,\\*,\\(,\\),\\,)': { 147 | '*': { 148 | type: 'array', 149 | isWildcard: true, 150 | properties: { 151 | a: { type: 'object' }, 152 | b: { type: 'object' }, 153 | '*': { type: 'object' }, 154 | '(': { type: 'object' }, 155 | ')': { type: 'object' }, 156 | ',': { type: 'object' } 157 | } 158 | } 159 | }, 160 | '\\\\': { 161 | '\\': { 162 | type: 'object' 163 | } 164 | }, 165 | 'foo*bar': { 166 | 'foo*bar': { 167 | type: 'object' 168 | } 169 | }, 170 | 'foo\\': { 171 | 'foo\\': { 172 | type: 'object' 173 | } 174 | }, 175 | // mask `\n`, should not resolve in a new line, 176 | // because we simply escape "n" character which has no meaning in our language 177 | '\\n': { 178 | n: { 179 | type: 'object' 180 | } 181 | }, 182 | 'multi\nline': { 183 | 'multi\nline': { 184 | type: 'object' 185 | } 186 | } 187 | } 188 | 189 | describe('compiler', function () { 190 | for (var name in tests) { 191 | if (!util.has(tests, name)) continue 192 | (function (name, test) { 193 | it('should compile ' + name, function () { 194 | assert.deepStrictEqual(compile(name), test) 195 | }) 196 | }(name, tests[name])) 197 | } 198 | }) 199 | -------------------------------------------------------------------------------- /test/cli-test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | const assert = require('assert').strict 3 | const path = require('path') 4 | const { exec, execFile, execFileSync } = require('child_process') 5 | 6 | // Win32: Replace cat with type and remove the single quotes surrounding echo 7 | const whenOnWindows = 8 | process.platform === 'win32' 9 | ? command => command.replace('cat ', 'type ').replace(/'|'/g, '') 10 | : command => command 11 | 12 | function cli (command, args, sync) { 13 | return new Promise(resolve => { 14 | if (sync) { 15 | try { 16 | // Don't pipe the stderr to this process by default 17 | // https://nodejs.org/api/child_process.html#child_process_child_process_execfilesync_file_args_options 18 | const stdout = execFileSync(command, args, { stdio: 'pipe' }).trim() 19 | resolve({ 20 | exitCode: 0, 21 | stdout 22 | }) 23 | } catch (error) { 24 | resolve({ 25 | exitCode: error.status, 26 | stdout: error.stdout.toString().trim(), 27 | stderr: error.stderr.toString().trim() 28 | }) 29 | } 30 | return 31 | } 32 | 33 | const handler = (error, stdout, stderr) => { 34 | if (error) { 35 | resolve({ 36 | error, 37 | exitCode: error.code, 38 | stdout: stdout.trim(), 39 | stderr: stderr.trim() 40 | }) 41 | } else { 42 | resolve({ 43 | exitCode: 0, 44 | json: JSON.parse(stdout) 45 | }) 46 | } 47 | } 48 | if (args) { 49 | // Remove stdin for child process or they will try to read from it until 50 | // we call stdin.end() from here. 51 | // https://github.com/nodejs/node/issues/2339#issuecomment-279235982 52 | execFile(command, args, { stdio: ['ignore', 'pipe', 'pipe'] }, handler) 53 | } else { 54 | exec(whenOnWindows(command), handler) 55 | } 56 | }) 57 | } 58 | 59 | const CLI_BIN = path.join(__dirname, '..', 'bin', 'json-mask.js') 60 | const FIXTURE_PATH = path.join(__dirname, 'fixture', 'activities.json') 61 | 62 | var tests = [ 63 | { 64 | it: 'should show fields missing error and usage information when no arguments specified', 65 | exec: { 66 | command: 'node', 67 | args: [CLI_BIN] 68 | }, 69 | e (result) { 70 | assert.strictEqual(result.exitCode, 1, 'exit code must be 1') 71 | assert.strictEqual(result.stderr, 'Fields argument missing') 72 | assert.ok(/usage:/i.test(result.stdout)) 73 | } 74 | }, 75 | { 76 | it: 'should show usage information when no file specified', 77 | exec: { 78 | command: 'node', 79 | args: [CLI_BIN, 'mask'], 80 | sync: true 81 | }, 82 | e (result) { 83 | assert.strictEqual(result.exitCode, 1, 'exit code must be 1') 84 | assert.strictEqual(result.stderr, 'Either pipe input into json-mask or specify a file as second argument') 85 | assert.ok(/usage:/i.test(result.stdout)) 86 | } 87 | }, 88 | { 89 | it: 'should read a file given as first argument', 90 | exec: { 91 | command: 'node', 92 | args: [CLI_BIN, 'kind', FIXTURE_PATH] 93 | }, 94 | e: { 95 | exitCode: 0, 96 | json: { 97 | kind: 'plus#activity' 98 | } 99 | } 100 | }, 101 | { 102 | it: 'should error with invalid JSON file input', 103 | exec: { 104 | command: 'node', 105 | args: [CLI_BIN, 'object', path.join(__dirname, 'fixture', 'invalid.json')] 106 | }, 107 | e (result) { 108 | assert.strictEqual(result.exitCode, 1, 'exit code must be 1') 109 | assert.ok(/Unexpected|Expected/.test(result.stderr)) 110 | assert.ok(/usage:/i.test(result.stdout)) 111 | } 112 | }, 113 | { 114 | it: 'should choke on empty input stream', 115 | exec: `echo '' | node ${CLI_BIN}`, 116 | mask: 's', 117 | e (result) { 118 | assert.strictEqual(result.exitCode, 1, 'exit code must be 1') 119 | assert.ok(/Unexpected|Expected/.test(result.stderr)) 120 | assert.ok(/usage:/i.test(result.stdout)) 121 | } 122 | }, 123 | { 124 | exec: `echo '{"s":"foo","n":666}' | node ${CLI_BIN}`, 125 | mask: 's', 126 | e: { 127 | exitCode: 0, 128 | json: { 129 | s: 'foo' 130 | } 131 | } 132 | }, 133 | { 134 | exec: `cat ${FIXTURE_PATH} | node ${CLI_BIN}`, 135 | mask: 'kind', 136 | e: { 137 | exitCode: 0, 138 | json: { 139 | kind: 'plus#activity' 140 | } 141 | } 142 | }, { 143 | exec: `cat ${FIXTURE_PATH} | node ${CLI_BIN}`, 144 | mask: 'object(objectType)', 145 | e: { 146 | exitCode: 0, 147 | json: { 148 | object: { objectType: 'note' } 149 | } 150 | } 151 | }, { 152 | exec: `cat ${FIXTURE_PATH} | node ${CLI_BIN}`, 153 | mask: 'url,object(content,attachments/url)', 154 | e: { 155 | exitCode: 0, 156 | json: { 157 | url: 'https://plus.google.com/102817283354809142195/posts/F97fqZwJESL', 158 | object: { 159 | content: 'Congratulations! You have successfully fetched an explicit public activity. The attached video is your reward. :)', 160 | attachments: [{ url: 'http://www.youtube.com/watch?v=dQw4w9WgXcQ' }] 161 | } 162 | } 163 | } 164 | } 165 | ] 166 | 167 | describe('cli', function () { 168 | var result, i 169 | for (i = 0; i < tests.length; i++) { 170 | (function (test) { 171 | it(test.it || 'should mask with ' + test.mask + ' in test #' + i, async function () { 172 | const command = (test.exec.command || test.exec) + (test.mask ? ` "${test.mask}"` : '') 173 | result = await cli(command, test.exec.args, test.exec.sync) 174 | const expect = typeof test.e === 'function' ? test.e : assert.deepStrictEqual 175 | expect(result, test.e) 176 | }) 177 | }(tests[i])) 178 | } 179 | }) 180 | -------------------------------------------------------------------------------- /test/index-test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | var mask = require('../lib') 4 | var assert = require('assert') 5 | var fixture = require('./fixture/activities.json') 6 | var tests 7 | 8 | function A () { 9 | this.a = 3 10 | this.b = 4 11 | } 12 | 13 | tests = [{ 14 | m: 'a', 15 | o: null, 16 | e: null 17 | }, { 18 | m: 'a', 19 | o: { b: 1 }, 20 | e: {} 21 | }, { 22 | m: 'a', 23 | o: { a: null, b: 1 }, 24 | e: { a: null } 25 | }, { 26 | m: 'a', 27 | o: [{ b: 1 }], 28 | e: [{}] 29 | }, { 30 | m: null, 31 | o: { a: 1 }, 32 | e: { a: 1 } 33 | }, { 34 | m: '', 35 | o: { a: 1 }, 36 | e: { a: 1 } 37 | }, { 38 | m: 'a', 39 | o: { a: 1, b: 1 }, 40 | e: { a: 1 } 41 | }, { 42 | m: 'notEmptyStr', 43 | o: { notEmptyStr: '' }, 44 | e: { notEmptyStr: '' } 45 | }, { 46 | m: 'notEmptyNum', 47 | o: { notEmptyNum: 0 }, 48 | e: { notEmptyNum: 0 } 49 | }, { 50 | m: 'a,b', 51 | o: { a: 1, b: 1, c: 1 }, 52 | e: { a: 1, b: 1 } 53 | }, { 54 | m: 'obj/s', 55 | o: { obj: { s: 1, t: 2 }, b: 1 }, 56 | e: { obj: { s: 1 } } 57 | }, { 58 | m: 'arr/s', 59 | o: { arr: [{ s: 1, t: 2 }, { s: 2, t: 3 }], b: 1 }, 60 | e: { arr: [{ s: 1 }, { s: 2 }] } 61 | }, { 62 | m: 'a/s/g,b', 63 | o: { a: { s: { g: 1, z: 1 } }, t: 2, b: 1 }, 64 | e: { a: { s: { g: 1 } }, b: 1 } 65 | }, { 66 | m: '*', 67 | o: { a: 2, b: null, c: 0, d: 3 }, 68 | e: { a: 2, b: null, c: 0, d: 3 } 69 | }, { 70 | m: 'a/*/g', 71 | o: { a: { s: { g: 3 }, t: { g: 4 }, u: { z: 1 } }, b: 1 }, 72 | e: { a: { s: { g: 3 }, t: { g: 4 }, u: {} } } 73 | }, { 74 | m: 'a/*', 75 | o: { a: { s: { g: 3 }, t: { g: 4 }, u: { z: 1 } }, b: 3 }, 76 | e: { a: { s: { g: 3 }, t: { g: 4 }, u: { z: 1 } } } 77 | }, { 78 | m: 'a(g)', 79 | o: { a: [{ g: 1, d: 2 }, { g: 2, d: 3 }] }, 80 | e: { a: [{ g: 1 }, { g: 2 }] } 81 | }, { 82 | m: 'a,c', 83 | o: { a: [], c: {} }, 84 | e: { a: [], c: {} } 85 | }, { 86 | m: 'b(d/*/z)', 87 | o: { b: [{ d: { g: { z: 22 }, b: 34 } }] }, 88 | e: { b: [{ d: { g: { z: 22 } } }] } 89 | }, { 90 | m: 'url,obj(url,a/url)', 91 | o: { url: 1, id: '1', obj: { url: 'h', a: [{ url: 1, z: 2 }], c: 3 } }, 92 | e: { url: 1, obj: { url: 'h', a: [{ url: 1 }] } } 93 | }, { 94 | m: '*(a,b)', 95 | o: { p1: { a: 1, b: 1, c: 1 }, p2: { a: 2, b: 2, c: 2 } }, 96 | e: { p1: { a: 1, b: 1 }, p2: { a: 2, b: 2 } } 97 | }, { 98 | m: 'kind', 99 | o: fixture, 100 | e: { kind: 'plus#activity' } 101 | }, { 102 | m: 'object(objectType)', 103 | o: fixture, 104 | e: { object: { objectType: 'note' } } 105 | }, { 106 | m: 'url,object(content,attachments/url)', 107 | o: fixture, 108 | e: { 109 | url: 'https://plus.google.com/102817283354809142195/posts/F97fqZwJESL', 110 | object: { 111 | content: 'Congratulations! You have successfully fetched an explicit public activity. The attached video is your reward. :)', 112 | attachments: [{ url: 'http://www.youtube.com/watch?v=dQw4w9WgXcQ' }] 113 | } 114 | } 115 | }, { 116 | m: 'i', 117 | o: [{ i: 1, o: 2 }, { i: 2, o: 2 }], 118 | e: [{ i: 1 }, { i: 2 }] 119 | }, { 120 | m: 'foo(bar)', 121 | o: { foo: { biz: 'bar' } }, 122 | e: { foo: {} } 123 | }, { 124 | m: 'foo(bar)', 125 | o: { foo: { biz: 'baz' } }, 126 | e: { foo: {} } 127 | }, { 128 | m: 'foobar,foobiz', 129 | o: { foobar: { foo: 'bar' }, foobiz: undefined }, 130 | e: { foobar: { foo: 'bar' } } 131 | }, { 132 | m: 'foobar', 133 | o: { foo: 'bar' }, 134 | e: {} 135 | }, { 136 | m: 'foobar', 137 | o: [{ biz: 'baz' }], 138 | e: [{}] 139 | }, { 140 | m: 'a', 141 | o: { a: [0, 0] }, 142 | e: { a: [0, 0] } 143 | }, { 144 | m: 'a', 145 | o: { a: [1, 0, 1] }, 146 | e: { a: [1, 0, 1] } 147 | }, { 148 | m: 'a/b', 149 | o: { a: new A() }, 150 | e: { a: { b: 4 } } 151 | }, { 152 | m: 'a(b/c),e', 153 | o: { a: [{ b: { c: 1 } }, { d: 2 }], e: 3, f: 4, g: 5 }, 154 | e: { a: [{ b: { c: 1 } }, {}], e: 3 } 155 | }, { 156 | m: 'a(b/c/d),e', 157 | o: { a: [{ b: { c: { d: 1 } } }, { d: 2 }], e: 3, f: 4, g: 5 }, 158 | e: { a: [{ b: { c: { d: 1 } } }, {}], e: 3 } 159 | }, { 160 | m: 'beta(first,second/third),cappa(first,second/third)', 161 | o: { 162 | alpha: 3, 163 | beta: { first: 'fv', second: { third: 'tv', fourth: 'fv' } }, 164 | cappa: { first: 'fv', second: { third: 'tv', fourth: 'fv' } } 165 | }, 166 | e: { 167 | beta: { first: 'fv', second: { third: 'tv' } }, 168 | cappa: { first: 'fv', second: { third: 'tv' } } 169 | } 170 | }, { 171 | m: 'a\\/b', 172 | o: { 'a/b': 1, c: 2 }, 173 | e: { 'a/b': 1 } 174 | }, { 175 | m: 'beta(first,second\\/third),cappa(first,second\\/third)', 176 | o: { 177 | alpha: 3, 178 | beta: { first: 'fv', 'second/third': 'tv', third: { fourth: 'fv' } }, 179 | cappa: { first: 'fv', 'second/third': 'tv', third: { fourth: 'fv' } } 180 | }, 181 | e: { 182 | beta: { first: 'fv', 'second/third': 'tv' }, 183 | cappa: { first: 'fv', 'second/third': 'tv' } 184 | } 185 | }, { 186 | m: '\\*', 187 | o: { '*': 101, beta: 'hidden' }, 188 | e: { '*': 101 } 189 | }, { 190 | m: 'first(\\*)', 191 | o: { first: { '*': 101, beta: 'hidden' } }, 192 | e: { first: { '*': 101 } } 193 | }, { 194 | m: 'some,\\*', 195 | o: { '*': 101, beta: 'hidden', some: 'visible' }, 196 | e: { '*': 101, some: 'visible' } 197 | }, { 198 | m: 'some,\\\\', 199 | o: { '\\': 120, beta: 'hidden', some: 'visible' }, 200 | e: { '\\': 120, some: 'visible' } 201 | }, { 202 | m: 'multi\nline(a)', 203 | o: { multi: 130, line: 131, 'multi\nline': { a: 135, b: 134 } }, 204 | e: { 'multi\nline': { a: 135 } } 205 | }, { 206 | m: 'a*', 207 | o: { 'a*': 1, b: 2 }, 208 | e: { 'a*': 1 } 209 | }, { 210 | m: '*a', 211 | o: { '*a': 1, b: 2 }, 212 | e: { '*a': 1 } 213 | }] 214 | 215 | describe('json-mask', function () { 216 | var result, i 217 | for (i = 0; i < tests.length; i++) { 218 | (function (test) { 219 | var testFunc = (test.__only) ? it.only : it 220 | testFunc('should mask ' + test.m + ' in test #' + i, function () { 221 | result = mask(test.o, test.m) 222 | assert.deepStrictEqual(result, test.e) 223 | }) 224 | }(tests[i])) 225 | } 226 | }) 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Mask [![Build Status](https://github.com/nemtsov/json-mask/actions/workflows/node.js.yml/badge.svg)](https://github.com/nemtsov/json-mask/actions/workflows/node.js.yml) [![NPM version](https://img.shields.io/npm/v/json-mask.svg)](https://www.npmjs.com/package/json-mask) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 2 | 3 | 4 | 5 | This is a tiny language and an engine for selecting specific parts of a JS object, hiding/masking the rest. 6 | 7 | ```js 8 | var mask = require('json-mask'); 9 | mask({ p: { a: 1, b: 2 }, z: 1 }, 'p/a,z'); // {p: {a: 1}, z: 1} 10 | ``` 11 | 12 | The main difference between JSONPath / JSONSelect and this engine is that JSON Mask 13 | **preserves the structure of the original input object**. 14 | Instead of returning an array of selected sub-elements (e.g. `[{a: 1}, {z: 1}]` from example above), 15 | it filters-out the parts of the object that you don't need, 16 | keeping the structure unchanged: `{p: {a: 1}, z: 1}`. 17 | 18 | This is important because JSON Mask was designed with HTTP resources in mind, 19 | the structure of which I didn't want to change after the unwanted fields 20 | were masked / filtered. 21 | 22 | If you've used the Google APIs, and provided a `?fields=` query-string to get a 23 | [Partial Response](https://developers.google.com/gdata/docs/2.0/reference#PartialResponse), you've 24 | already used this language. The desire to have partial responses in 25 | my own Node.js-based HTTP services was the reason I wrote JSON Mask. 26 | 27 | _For [express](http://expressjs.com/) users, there's an 28 | [express-partial-response](https://github.com/nemtsov/express-partial-response) middleware. 29 | It will integrate with your existing services with no additional code 30 | if you're using `res.json()` or `res.jsonp()`. And if you're already using [koa](https://github.com/koajs/koa.git) 31 | check out the [koa-json-mask](https://github.com/nemtsov/koa-json-mask) middleware._ 32 | 33 | This library has no dependencies. It works in Node as well as in the browser. 34 | 35 | **Note:** the 1.5KB (gz), or 4KB (uncompressed) browser build is in the `/build` folder. 36 | 37 | ## Syntax 38 | 39 | The syntax is loosely based on XPath: 40 | 41 | - `a,b,c` comma-separated list will select multiple fields 42 | - `a/b/c` path will select a field from its parent 43 | - `a(b,c)` sub-selection will select many fields from a parent 44 | - `a/*/c` the star `*` wildcard will select all items in a field 45 | 46 | Take a look at `test/index-test.js` for examples of all of these and more. 47 | 48 | ## Grammar 49 | 50 | ``` 51 | Props ::= Prop | Prop "," Props 52 | Prop ::= Object | Array 53 | Object ::= NAME | NAME "/" Prop 54 | Array ::= NAME "(" Props ")" 55 | NAME ::= ? all visible characters except "\" ? | EscapeSeq | Wildcard 56 | Wildcard ::= "*" 57 | EscapeSeq ::= "\" ? all visible characters ? 58 | ``` 59 | 60 | ## Examples 61 | 62 | Identify the fields you want to keep: 63 | 64 | ```js 65 | var fields = 'url,object(content,attachments/url)'; 66 | ``` 67 | 68 | From this sample object: 69 | 70 | ```js 71 | var originalObj = { 72 | id: 'z12gtjhq3qn2xxl2o224exwiqruvtda0i', 73 | url: 'https://plus.google.com/102817283354809142195/posts/F97fqZwJESL', 74 | object: { 75 | objectType: 'note', 76 | content: 77 | 'A picture... of a space ship... launched from earth 40 years ago.', 78 | attachments: [ 79 | { 80 | objectType: 'image', 81 | url: 'http://apod.nasa.gov/apod/ap110908.html', 82 | image: { height: 284, width: 506 } 83 | } 84 | ] 85 | }, 86 | provider: { title: 'Google+' } 87 | }; 88 | ``` 89 | 90 | Here's what you'll get back: 91 | 92 | ```js 93 | var expectObj = { 94 | url: 'https://plus.google.com/102817283354809142195/posts/F97fqZwJESL', 95 | object: { 96 | content: 97 | 'A picture... of a space ship... launched from earth 40 years ago.', 98 | attachments: [ 99 | { 100 | url: 'http://apod.nasa.gov/apod/ap110908.html' 101 | } 102 | ] 103 | } 104 | }; 105 | ``` 106 | 107 | Let's test that: 108 | 109 | ```js 110 | var mask = require('json-mask'); 111 | var assert = require('assert'); 112 | 113 | var maskedObj = mask(originalObj, fields); 114 | assert.deepEqual(maskedObj, expectObj); 115 | ``` 116 | 117 | ### Escaping 118 | 119 | It is also possible to get keys that contain `,*()/` using `\` (backslash) as escape character. 120 | 121 | ```json 122 | { 123 | "metadata": { 124 | "labels": { 125 | "app.kubernetes.io/name": "mysql", 126 | "location": "WH1" 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | You can filter out the location property by `metadata(labels(app.kubernetes.io\/name))` mask. 133 | 134 | NOTE: In [JavaScript String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#escape_sequences) you must escape backslash with another backslash: 135 | ```js 136 | var fields = 'metadata(labels(app.kubernetes.io\\/name))' 137 | ``` 138 | 139 | ### Partial Responses Server Example 140 | 141 | Here's an example of using `json-mask` to implement the 142 | [Google API Partial Response](https://developers.google.com/gdata/docs/2.0/reference#PartialResponse) 143 | 144 | ```js 145 | var http = require('http'); 146 | var url = require('url'); 147 | var mask = require('json-mask'); 148 | var server; 149 | 150 | server = http.createServer(function(req, res) { 151 | var fields = url.parse(req.url, true).query.fields; 152 | var data = { 153 | firstName: 'Mohandas', 154 | lastName: 'Gandhi', 155 | aliases: [ 156 | { 157 | firstName: 'Mahatma', 158 | lastName: 'Gandhi' 159 | }, 160 | { 161 | firstName: 'Bapu' 162 | } 163 | ] 164 | }; 165 | res.writeHead(200, { 'Content-Type': 'application/json' }); 166 | res.end(JSON.stringify(mask(data, fields))); 167 | }); 168 | 169 | server.listen(4000); 170 | ``` 171 | 172 | Let's test it: 173 | 174 | ```bash 175 | $ curl 'http://localhost:4000' 176 | {"firstName":"Mohandas","lastName":"Gandhi","aliases":[{"firstName":"Mahatma","lastName":"Gandhi"},{"firstName":"Bapu"}]} 177 | 178 | $ # Let's just get the first name 179 | $ curl 'http://localhost:4000?fields=lastName' 180 | {"lastName":"Gandhi"} 181 | 182 | $ # Now, let's just get the first names directly as well as from aliases 183 | $ curl 'http://localhost:4000?fields=firstName,aliases(firstName)' 184 | {"firstName":"Mohandas","aliases":[{"firstName":"Mahatma"},{"firstName":"Bapu"}]} 185 | ``` 186 | 187 | **Note:** a few more examples are in the `/example` folder. 188 | 189 | ## Command Line Interface - CLI 190 | 191 | When installed globally using `npm i -g json-mask` you can use it like: 192 | 193 | `json-mask "" []` 194 | 195 | ### Examples 196 | 197 | Stream from online resource: 198 | 199 | `curl https://api.myjson.com/bins/krrxw | json-mask "url,object(content,attachments/url)"` 200 | 201 | Read from file and write to output file: 202 | 203 | `json-mask "url,object(content,attachments/url)" input.json > output.json` 204 | 205 | Read from file and print redirect to file: 206 | 207 | `json-mask "url,object(content,attachments/url)" input.json > output.json` 208 | 209 | ## CDN 210 | 211 | **unpkg** 212 | 213 | - `https://unpkg.com/json-mask/build/jsonMask.js` 214 | - `https://unpkg.com/json-mask/build/jsonMask.min.js` 215 | 216 | ## License 217 | 218 | [MIT](/LICENSE) 219 | --------------------------------------------------------------------------------