├── 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 [](https://github.com/nemtsov/json-mask/actions/workflows/node.js.yml) [](https://www.npmjs.com/package/json-mask) [](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 "" [