@port-labs/jq-node-bindings 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/index.d.ts +2 -1
- package/lib/index.js +4 -16
- package/lib/jq.js +21 -0
- package/lib/template.js +108 -0
- package/package.json +3 -3
- package/test/santiy.test.js +15 -0
- package/test/template.test.js +140 -0
package/README.md
CHANGED
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
This is a library for Node.js that provides C bindings to the [jq](https://stedolan.github.io/jq/) library. It allows you to use jq to extract and manipulate data from JSON objects in Node.js.
|
|
4
4
|
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
npm install @port-labs/jq-node-bindings
|
|
9
|
-
```
|
|
10
|
-
|
|
11
5
|
## Requirements
|
|
12
6
|
|
|
13
7
|
To use this library, you must have `node-gyp` installed on your system. and the following libraries
|
|
@@ -49,14 +43,18 @@ To use this library, you must have `node-gyp` installed on your system. and the
|
|
|
49
43
|
sudo apt-get install -y autoconf make libtool automake
|
|
50
44
|
```
|
|
51
45
|
|
|
46
|
+
## Installation
|
|
52
47
|
|
|
48
|
+
```
|
|
49
|
+
npm install @port-labs/jq-node-bindings
|
|
50
|
+
```
|
|
53
51
|
|
|
54
52
|
## Usage
|
|
55
53
|
|
|
56
54
|
Here's an example of how to use the library:
|
|
57
55
|
|
|
58
56
|
```typescript
|
|
59
|
-
import { exec } from 'jq-node-bindings';
|
|
57
|
+
import { exec } from '@port-labs/jq-node-bindings';
|
|
60
58
|
|
|
61
59
|
const json = { foo: 'bar' };
|
|
62
60
|
const input = '.foo';
|
package/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
declare module '@port-labs/jq-node-bindings' {
|
|
2
|
-
export function exec(json: object, input: string): object | Array<any> | string | number | boolean | null;
|
|
2
|
+
export function exec(json: object, input: string, options?: {enableEnv?: boolean}): object | Array<any> | string | number | boolean | null;
|
|
3
|
+
export function renderRecursively(json: object, input: object | Array<any> | string | number | boolean | null): object | Array<any> | string | number | boolean | null;
|
|
3
4
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const escapeFilter = (filter) => {
|
|
4
|
-
// Escape single quotes only if they are opening or closing a string
|
|
5
|
-
return filter.replace(/^'|'$|(?<=\W)'(?=[^"]*$)|"(?=[^"]*'$)/g, '"');
|
|
6
|
-
}
|
|
1
|
+
const jq = require('./jq');
|
|
2
|
+
const template = require('./template');
|
|
7
3
|
|
|
8
4
|
|
|
9
5
|
module.exports = {
|
|
10
|
-
exec:
|
|
11
|
-
|
|
12
|
-
const data = nativeJq.exec(JSON.stringify(object), escapeFilter(filter))
|
|
13
|
-
|
|
14
|
-
return data?.value;
|
|
15
|
-
} catch (err) {
|
|
16
|
-
return null
|
|
17
|
-
}
|
|
18
|
-
}
|
|
6
|
+
exec: jq.exec,
|
|
7
|
+
renderRecursively: template.renderRecursively
|
|
19
8
|
};
|
|
20
|
-
|
package/lib/jq.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const nativeJq = require('bindings')('jq-node-bindings')
|
|
2
|
+
|
|
3
|
+
const formatFilter = (filter, options) => {
|
|
4
|
+
// Escape single quotes only if they are opening or closing a string
|
|
5
|
+
let formattedFilter = filter.replace(/(^|\s)'(?!\s|")|(?<!\s|")'(\s|$)/g, '$1"$2');
|
|
6
|
+
// Conditionally enable access to env
|
|
7
|
+
return options.enableEnv ? formattedFilter: `def env: {}; {} as $ENV | ${formattedFilter}`;
|
|
8
|
+
}
|
|
9
|
+
const exec = (object, filter, options = { enableEnv: false }) => {
|
|
10
|
+
try {
|
|
11
|
+
const data = nativeJq.exec(JSON.stringify(object), formatFilter(filter, options))
|
|
12
|
+
|
|
13
|
+
return data?.value;
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
exec
|
|
21
|
+
};
|
package/lib/template.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const jq = require('./jq');
|
|
2
|
+
|
|
3
|
+
const findInsideDoubleBracesIndices = (input) => {
|
|
4
|
+
let wrappingQuote = null;
|
|
5
|
+
let insideDoubleBracesStart = null;
|
|
6
|
+
const indices = [];
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
9
|
+
const char = input[i];
|
|
10
|
+
|
|
11
|
+
if (char === '"' || char === "'") {
|
|
12
|
+
// If inside quotes, ignore braces
|
|
13
|
+
if (!wrappingQuote) {
|
|
14
|
+
wrappingQuote = char;
|
|
15
|
+
} else if (wrappingQuote === char) {
|
|
16
|
+
wrappingQuote = null;
|
|
17
|
+
}
|
|
18
|
+
} else if (!wrappingQuote && char === '{' && i > 0 && input[i - 1] === '{') {
|
|
19
|
+
// if opening double braces that not wrapped with quotes
|
|
20
|
+
if (insideDoubleBracesStart) {
|
|
21
|
+
throw new Error(`Found double braces in index ${i - 1} inside other one in index ${insideDoubleBracesStart - '{{'.length}`);
|
|
22
|
+
}
|
|
23
|
+
insideDoubleBracesStart = i + 1;
|
|
24
|
+
if (input[i + 1] === '{') {
|
|
25
|
+
// To overcome three "{" in a row considered as two different opening double braces
|
|
26
|
+
i += 1;
|
|
27
|
+
}
|
|
28
|
+
} else if (!wrappingQuote && char === '}' && i > 0 && input[i - 1] === '}') {
|
|
29
|
+
// if closing double braces that not wrapped with quotes
|
|
30
|
+
if (insideDoubleBracesStart) {
|
|
31
|
+
indices.push({start: insideDoubleBracesStart, end: i - 1});
|
|
32
|
+
insideDoubleBracesStart = null;
|
|
33
|
+
if (input[i + 1] === '}') {
|
|
34
|
+
// To overcome three "}" in a row considered as two different closing double braces
|
|
35
|
+
i += 1;
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error(`Found closing double braces in index ${i - 1} without opening double braces`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (insideDoubleBracesStart) {
|
|
44
|
+
throw new Error(`Found opening double braces in index ${insideDoubleBracesStart - '{{'.length} without closing double braces`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return indices;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const render = (inputJson, template) => {
|
|
51
|
+
if (typeof template !== 'string') {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const indices = findInsideDoubleBracesIndices(template);
|
|
55
|
+
if (!indices.length) {
|
|
56
|
+
// If no jq templates in string, return it
|
|
57
|
+
return template;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const firstIndex = indices[0];
|
|
61
|
+
if (indices.length === 1 && template.trim().startsWith('{{') && template.trim().endsWith('}}')) {
|
|
62
|
+
// If entire string is a template, evaluate and return the result with the original type
|
|
63
|
+
return jq.exec(inputJson, template.slice(firstIndex.start, firstIndex.end));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let result = template.slice(0, firstIndex.start - '{{'.length); // Initiate result with string until first template start index
|
|
67
|
+
indices.forEach((index, i) => {
|
|
68
|
+
const jqResult = jq.exec(inputJson, template.slice(index.start, index.end));
|
|
69
|
+
result +=
|
|
70
|
+
// Add to the result the stringified evaluated jq of the current template
|
|
71
|
+
(typeof jqResult === 'string' ? jqResult : JSON.stringify(jqResult)) +
|
|
72
|
+
// Add to the result from template end index. if last template index - until the end of string, else until next start index
|
|
73
|
+
template.slice(
|
|
74
|
+
index.end + '}}'.length,
|
|
75
|
+
i + 1 === indices.length ? template.length : indices[i + 1].start - '{{'.length,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const renderRecursively = (inputJson, template) => {
|
|
83
|
+
if (typeof template === 'string') {
|
|
84
|
+
return render(inputJson, template);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(template)) {
|
|
87
|
+
return template.map((value) => renderRecursively(inputJson, value));
|
|
88
|
+
}
|
|
89
|
+
if (typeof template === 'object' && template !== null) {
|
|
90
|
+
return Object.fromEntries(
|
|
91
|
+
Object.entries(template).flatMap(([key, value]) => {
|
|
92
|
+
const evaluatedKey = renderRecursively(inputJson, key);
|
|
93
|
+
if (!['undefined', 'string'].includes(typeof evaluatedKey) && evaluatedKey !== null) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Evaluated object key should be undefined, null or string. Original key: ${key}, evaluated to: ${JSON.stringify(evaluatedKey)}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return evaluatedKey ? [[evaluatedKey, renderRecursively(inputJson, value)]] : [];
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return template;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
renderRecursively
|
|
108
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@port-labs/jq-node-bindings",
|
|
3
|
-
"version": "v0.0.
|
|
3
|
+
"version": "v0.0.9",
|
|
4
4
|
"description": "Node.js bindings for JQ",
|
|
5
|
-
"jq-node-bindings": "0.0.
|
|
5
|
+
"jq-node-bindings": "0.0.9",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"configure": "node-gyp configure",
|
|
@@ -45,4 +45,4 @@
|
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=6.0.0"
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|
package/test/santiy.test.js
CHANGED
|
@@ -142,5 +142,20 @@ describe('jq', () => {
|
|
|
142
142
|
|
|
143
143
|
expect(result).toBe('foo');
|
|
144
144
|
})
|
|
145
|
+
|
|
146
|
+
it ('Testing multiple the \'\' in the same expression', () => {
|
|
147
|
+
const json = { foo: 'bar' };
|
|
148
|
+
const input = "'https://some.random.url' + .foo + '-1' + '.' + .foo + '.' + 'longgggg' + .foo + ')test(' + .foo + 'testadsftets'";
|
|
149
|
+
const result = jq.exec(json, input);
|
|
150
|
+
|
|
151
|
+
expect(result).toBe('https://some.random.urlbar-1.bar.longggggbar)test(bartestadsftets');
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('test disable env', () => {
|
|
155
|
+
expect(jq.exec({}, 'env', {enableEnv: false})).toEqual({});
|
|
156
|
+
expect(jq.exec({}, 'env', {enableEnv: true})).not.toEqual({});
|
|
157
|
+
expect(jq.exec({}, 'env', {})).toEqual({});
|
|
158
|
+
expect(jq.exec({}, 'env')).toEqual({});
|
|
159
|
+
})
|
|
145
160
|
})
|
|
146
161
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const jq = require('../lib');
|
|
2
|
+
|
|
3
|
+
describe('template', () => {
|
|
4
|
+
it('should break', () => {
|
|
5
|
+
const json = { foo2: 'bar' };
|
|
6
|
+
const input = '{{.foo}}';
|
|
7
|
+
const result = jq.renderRecursively(json, input);
|
|
8
|
+
|
|
9
|
+
expect(result).toBe(null);
|
|
10
|
+
});
|
|
11
|
+
it('non template should work', () => {
|
|
12
|
+
const json = { foo2: 'bar' };
|
|
13
|
+
const render = (input) => jq.renderRecursively(json, input);
|
|
14
|
+
|
|
15
|
+
expect(render(123)).toBe(123);
|
|
16
|
+
expect(render(undefined)).toBe(undefined);
|
|
17
|
+
expect(render(null)).toBe(null);
|
|
18
|
+
expect(render(true)).toBe(true);
|
|
19
|
+
expect(render(false)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('different types should work', () => {
|
|
22
|
+
const input = '{{.foo}}';
|
|
23
|
+
const render = (json) => jq.renderRecursively(json, input);
|
|
24
|
+
|
|
25
|
+
expect(render({ foo: 'bar' })).toBe('bar');
|
|
26
|
+
expect(render({ foo: 1 })).toBe(1);
|
|
27
|
+
expect(render({ foo: true })).toBe(true);
|
|
28
|
+
expect(render({ foo: null })).toBe(null);
|
|
29
|
+
expect(render({ foo: undefined })).toBe(null);
|
|
30
|
+
expect(render({ foo: ['bar'] })).toEqual(['bar']);
|
|
31
|
+
expect(render({ foo: [{ bar: 'bar' }] })).toEqual([{ bar: 'bar' }]);
|
|
32
|
+
expect(render({ foo: {prop1: "1"} })).toEqual({prop1: "1"});
|
|
33
|
+
expect(render({ foo: {obj: { obj2: { num: 1, string: "str"} }} })).toEqual({obj: { obj2: { num: 1, string: "str"} }});
|
|
34
|
+
expect(render({ foo: { obj: { obj2: { num: 1, string: "str", bool: true} }} })).toEqual({ obj: { obj2: { num: 1, string: "str", bool: true} }});
|
|
35
|
+
});
|
|
36
|
+
it ('should return undefined', () => {
|
|
37
|
+
const json = { foo: 'bar' };
|
|
38
|
+
const input = '{{empty}}';
|
|
39
|
+
const result = jq.renderRecursively(json, input);
|
|
40
|
+
|
|
41
|
+
expect(result).toBe(undefined);
|
|
42
|
+
});
|
|
43
|
+
it ('should return null on invalid json', () => {
|
|
44
|
+
const json = "foo";
|
|
45
|
+
const input = '{{.foo}}';
|
|
46
|
+
const result = jq.renderRecursively(json, input);
|
|
47
|
+
|
|
48
|
+
expect(result).toBe(undefined);
|
|
49
|
+
});
|
|
50
|
+
it('should excape \'\' to ""', () => {
|
|
51
|
+
const json = { foo: 'com' };
|
|
52
|
+
const input = "{{'https://url.' + .foo}}";
|
|
53
|
+
const result = jq.renderRecursively(json, input);
|
|
54
|
+
|
|
55
|
+
expect(result).toBe('https://url.com');
|
|
56
|
+
});
|
|
57
|
+
it('should not escape \' in the middle of the string', () => {
|
|
58
|
+
const json = { foo: 'com' };
|
|
59
|
+
const input = "{{\"https://'url.\" + 'test.' + .foo}}";
|
|
60
|
+
const result = jq.renderRecursively(json, input);
|
|
61
|
+
|
|
62
|
+
expect(result).toBe("https://'url.test.com");
|
|
63
|
+
});
|
|
64
|
+
it ('should run a jq function succesfully', () => {
|
|
65
|
+
const json = { foo: 'bar' };
|
|
66
|
+
const input = '{{.foo | gsub("bar";"foo")}}';
|
|
67
|
+
const result = jq.renderRecursively(json, input);
|
|
68
|
+
|
|
69
|
+
expect(result).toBe('foo');
|
|
70
|
+
});
|
|
71
|
+
it ('Testing multiple the \'\' in the same expression', () => {
|
|
72
|
+
const json = { foo: 'bar' };
|
|
73
|
+
const input = "{{'https://some.random.url' + .foo + '-1' + '.' + .foo + '.' + 'longgggg' + .foo + ')test(' + .foo + 'testadsftets'}}";
|
|
74
|
+
const result = jq.renderRecursively(json, input);
|
|
75
|
+
|
|
76
|
+
expect(result).toBe('https://some.random.urlbar-1.bar.longggggbar)test(bartestadsftets');
|
|
77
|
+
});
|
|
78
|
+
it ('Testing multiple the \'\' in the same expression', () => {
|
|
79
|
+
const json = { foo: 'bar' };
|
|
80
|
+
const input = "{{'https://some.random.url' + .foo + '-1' + '.' + .foo + '.' + 'longgggg' + .foo + ')test(' + .foo + 'testadsftets'}}";
|
|
81
|
+
const result = jq.renderRecursively(json, input);
|
|
82
|
+
|
|
83
|
+
expect(result).toBe('https://some.random.urlbar-1.bar.longggggbar)test(bartestadsftets');
|
|
84
|
+
});
|
|
85
|
+
it('should break for invalid template', () => {
|
|
86
|
+
const json = { foo: 'bar' };
|
|
87
|
+
const render = (input) => () => jq.renderRecursively(json, input);
|
|
88
|
+
|
|
89
|
+
expect(render('prefix{{.foo}postfix')).toThrow('Found opening double braces in index 6 without closing double braces');
|
|
90
|
+
expect(render('prefix{.foo}}postfix')).toThrow('Found closing double braces in index 11 without opening double braces');
|
|
91
|
+
expect(render('prefix{{ .foo {{ }}postfix')).toThrow('Found double braces in index 14 inside other one in index 6');
|
|
92
|
+
expect(render('prefix{{ .foo }} }}postfix')).toThrow('Found closing double braces in index 17 without opening double braces');
|
|
93
|
+
expect(render('prefix{{ .foo }} }}postfix')).toThrow('Found closing double braces in index 17 without opening double braces');
|
|
94
|
+
expect(render('prefix{{ "{{" + .foo }} }}postfix')).toThrow('Found closing double braces in index 24 without opening double braces');
|
|
95
|
+
expect(render('prefix{{ \'{{\' + .foo }} }}postfix')).toThrow('Found closing double braces in index 24 without opening double braces');
|
|
96
|
+
expect(render({'{{1}}': 'bar'})).toThrow('Evaluated object key should be undefined, null or string. Original key: {{1}}, evaluated to: 1');
|
|
97
|
+
expect(render({'{{true}}': 'bar'})).toThrow('Evaluated object key should be undefined, null or string. Original key: {{true}}, evaluated to: true');
|
|
98
|
+
expect(render({'{{ {} }}': 'bar'})).toThrow('Evaluated object key should be undefined, null or string. Original key: {{ {} }}, evaluated to: {}');
|
|
99
|
+
});
|
|
100
|
+
it('should concat string and other types', () => {
|
|
101
|
+
const input = 'https://some.random.url?q={{.foo}}';
|
|
102
|
+
const render = (json) => jq.renderRecursively(json, input);
|
|
103
|
+
|
|
104
|
+
expect(render({ foo: 'bar' })).toBe('https://some.random.url?q=bar');
|
|
105
|
+
expect(render({ foo: 1 })).toBe('https://some.random.url?q=1');
|
|
106
|
+
expect(render({ foo: false })).toBe('https://some.random.url?q=false');
|
|
107
|
+
expect(render({ foo: null })).toBe('https://some.random.url?q=null');
|
|
108
|
+
expect(render({ foo: undefined })).toBe('https://some.random.url?q=null');
|
|
109
|
+
expect(render({ foo: [1] })).toBe('https://some.random.url?q=[1]');
|
|
110
|
+
expect(render({ foo: {bar: 'bar'} })).toBe('https://some.random.url?q={\"bar\":\"bar\"}');
|
|
111
|
+
});
|
|
112
|
+
it('testing multiple template blocks', () => {
|
|
113
|
+
const json = {str: 'bar', num: 1, bool: true, 'null': null, arr: ['foo'], obj: {bar: 'bar'}};
|
|
114
|
+
const input = 'https://some.random.url?str={{.str}}&num={{.num}}&bool={{.bool}}&null={{.null}}&arr={{.arr}}&obj={{.obj}}';
|
|
115
|
+
const result = jq.renderRecursively(json, input);
|
|
116
|
+
|
|
117
|
+
expect(result).toBe("https://some.random.url?str=bar&num=1&bool=true&null=null&arr=[\"foo\"]&obj={\"bar\":\"bar\"}");
|
|
118
|
+
});
|
|
119
|
+
it('testing conditional key', () => {
|
|
120
|
+
const json = {};
|
|
121
|
+
const render = (input) => jq.renderRecursively(json, input);
|
|
122
|
+
|
|
123
|
+
expect(render({'{{empty}}': 'bar'})).toEqual({});
|
|
124
|
+
expect(render({'{{null}}': 'bar'})).toEqual({});
|
|
125
|
+
expect(render({'{{""}}': 'bar'})).toEqual({});
|
|
126
|
+
expect(render({'{{\'\'}}': 'bar'})).toEqual({});
|
|
127
|
+
});
|
|
128
|
+
it('recursive templates should work', () => {
|
|
129
|
+
const json = { foo: 'bar', bar: 'foo' };
|
|
130
|
+
const render = (input) => jq.renderRecursively(json, input);
|
|
131
|
+
|
|
132
|
+
expect(render({'{{.foo}}': '{{.bar}}{{.foo}}'})).toEqual({bar: 'foobar'});
|
|
133
|
+
expect(render({'{{.foo}}': {foo: '{{.foo}}'}})).toEqual({bar: {foo: 'bar'}});
|
|
134
|
+
expect(render([1, true, null, undefined, '{{.foo}}', 'https://{{.bar}}.com'])).toEqual([1, true, null, undefined, 'bar', 'https://foo.com']);
|
|
135
|
+
expect(render([['{{.bar}}{{.foo}}'], 1, '{{.bar | ascii_upcase}}'])).toEqual([['foobar'], 1, 'FOO']);
|
|
136
|
+
expect(render([{'{{.bar}}': [false, '/foo/{{.foo + .bar}}']}])).toEqual([{foo: [false, '/foo/barfoo']}]);
|
|
137
|
+
expect(render({foo: [{bar: '{{1}}'}, '{{empty}}']})).toEqual({foo: [{bar: 1}, undefined]});
|
|
138
|
+
});
|
|
139
|
+
})
|
|
140
|
+
|