@port-labs/jq-node-bindings 0.0.8 → 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 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 nativeJq = require('bindings')('jq-node-bindings')
2
-
3
- const escapeFilter = (filter) => {
4
- // Escape single quotes only if they are opening or closing a string
5
- return filter.replace(/(^|\s)'(?!\s|")|(?<!\s|")'(\s|$)/g, '$1"$2');
6
- }
1
+ const jq = require('./jq');
2
+ const template = require('./template');
7
3
 
8
4
 
9
5
  module.exports = {
10
- exec: (object, filter) => {
11
- try {
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
+ };
@@ -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.8",
3
+ "version": "v0.0.9",
4
4
  "description": "Node.js bindings for JQ",
5
- "jq-node-bindings": "0.0.8",
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
+ }
@@ -150,5 +150,12 @@ describe('jq', () => {
150
150
 
151
151
  expect(result).toBe('https://some.random.urlbar-1.bar.longggggbar)test(bartestadsftets');
152
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
+ })
153
160
  })
154
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
+