@port-labs/jq-node-bindings 0.0.9 → 0.0.11

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/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  declare module '@port-labs/jq-node-bindings' {
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;
2
+ type ExecOptions = { enableEnv?: boolean, throwOnError?: boolean };
3
+
4
+ export function exec(json: object, input: string, options?: ExecOptions): object | Array<any> | string | number | boolean | null;
5
+
6
+ export function renderRecursively(json: object, input: object | Array<any> | string | number | boolean | null, execOptions?: ExecOptions): object | Array<any> | string | number | boolean | null;
4
7
  }
package/lib/jq.js CHANGED
@@ -1,17 +1,20 @@
1
1
  const nativeJq = require('bindings')('jq-node-bindings')
2
2
 
3
- const formatFilter = (filter, options) => {
3
+ const formatFilter = (filter, {enableEnv = false} = {}) => {
4
4
  // Escape single quotes only if they are opening or closing a string
5
5
  let formattedFilter = filter.replace(/(^|\s)'(?!\s|")|(?<!\s|")'(\s|$)/g, '$1"$2');
6
6
  // Conditionally enable access to env
7
- return options.enableEnv ? formattedFilter: `def env: {}; {} as $ENV | ${formattedFilter}`;
7
+ return enableEnv ? formattedFilter : `def env: {}; {} as $ENV | ${formattedFilter}`;
8
8
  }
9
- const exec = (object, filter, options = { enableEnv: false }) => {
9
+ const exec = (object, filter, {enableEnv = false, throwOnError = false} = {}) => {
10
10
  try {
11
- const data = nativeJq.exec(JSON.stringify(object), formatFilter(filter, options))
11
+ const data = nativeJq.exec(JSON.stringify(object), formatFilter(filter, {enableEnv}))
12
12
 
13
13
  return data?.value;
14
14
  } catch (err) {
15
+ if (throwOnError) {
16
+ throw err;
17
+ }
15
18
  return null
16
19
  }
17
20
  }
package/lib/template.js CHANGED
@@ -8,8 +8,12 @@ const findInsideDoubleBracesIndices = (input) => {
8
8
  for (let i = 0; i < input.length; i += 1) {
9
9
  const char = input[i];
10
10
 
11
- if (char === '"' || char === "'") {
12
- // If inside quotes, ignore braces
11
+ if (insideDoubleBracesStart && char === '\\') {
12
+ // If next character is escaped, skip it
13
+ i += 1;
14
+ }
15
+ if (insideDoubleBracesStart && (char === '"' || char === "'")) {
16
+ // If inside double braces and inside quotes, ignore braces
13
17
  if (!wrappingQuote) {
14
18
  wrappingQuote = char;
15
19
  } else if (wrappingQuote === char) {
@@ -47,7 +51,7 @@ const findInsideDoubleBracesIndices = (input) => {
47
51
  return indices;
48
52
  }
49
53
 
50
- const render = (inputJson, template) => {
54
+ const render = (inputJson, template, execOptions = {}) => {
51
55
  if (typeof template !== 'string') {
52
56
  return null;
53
57
  }
@@ -60,12 +64,12 @@ const render = (inputJson, template) => {
60
64
  const firstIndex = indices[0];
61
65
  if (indices.length === 1 && template.trim().startsWith('{{') && template.trim().endsWith('}}')) {
62
66
  // 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));
67
+ return jq.exec(inputJson, template.slice(firstIndex.start, firstIndex.end), execOptions);
64
68
  }
65
69
 
66
70
  let result = template.slice(0, firstIndex.start - '{{'.length); // Initiate result with string until first template start index
67
71
  indices.forEach((index, i) => {
68
- const jqResult = jq.exec(inputJson, template.slice(index.start, index.end));
72
+ const jqResult = jq.exec(inputJson, template.slice(index.start, index.end), execOptions);
69
73
  result +=
70
74
  // Add to the result the stringified evaluated jq of the current template
71
75
  (typeof jqResult === 'string' ? jqResult : JSON.stringify(jqResult)) +
@@ -79,24 +83,24 @@ const render = (inputJson, template) => {
79
83
  return result;
80
84
  }
81
85
 
82
- const renderRecursively = (inputJson, template) => {
86
+ const renderRecursively = (inputJson, template, execOptions = {}) => {
83
87
  if (typeof template === 'string') {
84
- return render(inputJson, template);
88
+ return render(inputJson, template, execOptions);
85
89
  }
86
90
  if (Array.isArray(template)) {
87
- return template.map((value) => renderRecursively(inputJson, value));
91
+ return template.map((value) => renderRecursively(inputJson, value, execOptions));
88
92
  }
89
93
  if (typeof template === 'object' && template !== null) {
90
94
  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
- }),
95
+ Object.entries(template).flatMap(([key, value]) => {
96
+ const evaluatedKey = renderRecursively(inputJson, key, execOptions);
97
+ if (!['undefined', 'string'].includes(typeof evaluatedKey) && evaluatedKey !== null) {
98
+ throw new Error(
99
+ `Evaluated object key should be undefined, null or string. Original key: ${key}, evaluated to: ${JSON.stringify(evaluatedKey)}`,
100
+ );
101
+ }
102
+ return evaluatedKey ? [[evaluatedKey, renderRecursively(inputJson, value, execOptions)]] : [];
103
+ }),
100
104
  );
101
105
  }
102
106
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@port-labs/jq-node-bindings",
3
- "version": "v0.0.9",
3
+ "version": "v0.0.11",
4
4
  "description": "Node.js bindings for JQ",
5
- "jq-node-bindings": "0.0.9",
5
+ "jq-node-bindings": "0.0.11",
6
6
  "main": "lib/index.js",
7
7
  "scripts": {
8
8
  "configure": "node-gyp configure",
package/src/binding.cc CHANGED
@@ -56,6 +56,20 @@ void jv_object_to_v8(std::string key, jv actual, v8::Local<v8::Object> ret) {
56
56
  v8::Local<v8::Value> v8_val;
57
57
 
58
58
  switch (k) {
59
+ case JV_KIND_INVALID: {
60
+ jv msg = jv_invalid_get_msg(jv_copy(actual));
61
+ char err[4096];
62
+ if (jv_get_kind(msg) == JV_KIND_STRING) {
63
+ snprintf(err, sizeof(err), "jq: error: %s", jv_string_value(msg));
64
+ jv_free(msg);
65
+ jv_free(actual);
66
+ Nan::ThrowTypeError(err);
67
+ return;
68
+ }
69
+ jv_free(msg);
70
+ jv_free(actual);
71
+ break;
72
+ }
59
73
  case JV_KIND_NULL: {
60
74
  v8_val = Nan::Null();
61
75
  break;
@@ -104,40 +118,55 @@ void jv_object_to_v8(std::string key, jv actual, v8::Local<v8::Object> ret) {
104
118
  Nan::Set(ret, v8_key, v8_val);
105
119
  }
106
120
 
121
+ struct err_data {
122
+ char buf[4096];
123
+ };
124
+
125
+ void throw_err_cb(void *data, jv msg) {
126
+ struct err_data *err_data = (struct err_data *)data;
127
+ if (jv_get_kind(msg) != JV_KIND_STRING)
128
+ msg = jv_dump_string(msg, JV_PRINT_INVALID);
129
+ if (!strncmp(jv_string_value(msg), "jq: error", sizeof("jq: error") - 1))
130
+ snprintf(err_data->buf, sizeof(err_data->buf), "%s", jv_string_value(msg));
131
+ if (strchr(err_data->buf, '\n'))
132
+ *(strchr(err_data->buf, '\n')) = '\0';
133
+ jv_free(msg);
134
+ }
135
+
107
136
  void jq_exec(std::string json, std::string filter,const Nan::FunctionCallbackInfo<v8::Value>& info) {
108
137
  jq_state *jq = NULL;
138
+ struct err_data err_msg;
109
139
 
110
140
  if (cache.exist(filter)) {
111
141
  jq = cache.get(filter);
112
142
  } else {
113
143
  jq = jq_init();
144
+ jq_set_error_cb(jq, throw_err_cb, &err_msg);
114
145
  if (!jq_compile(jq, filter.c_str())) {
115
- info.GetReturnValue().Set(Nan::Null());
146
+ Nan::ThrowTypeError(err_msg.buf);
116
147
  return;
117
148
  }
118
149
  cache.put(filter, jq);
119
150
  }
120
151
 
121
- jv input = jv_parse(json.c_str());
122
-
123
- if (!jv_is_valid(input)) {
152
+ if (jq == NULL) {
124
153
  info.GetReturnValue().Set(Nan::Null());
125
154
  return;
126
155
  }
127
156
 
128
- if (jq == NULL) {
157
+ jv input = jv_parse(json.c_str());
158
+
159
+ if (!jv_is_valid(input)) {
129
160
  info.GetReturnValue().Set(Nan::Null());
161
+ jv_free(input);
130
162
  return;
131
163
  }
132
164
 
133
165
  jq_start(jq, input, 0);
134
-
135
- jv actual = jq_next(jq);
136
- jv_kind k = jv_get_kind(actual);
166
+ jv result = jq_next(jq);
137
167
 
138
168
  v8::Local<v8::Object> ret = Nan::New<v8::Object>();
139
-
140
- jv_object_to_v8("value", actual, ret);
169
+ jv_object_to_v8("value", result, ret);
141
170
 
142
171
  info.GetReturnValue().Set(ret);
143
172
  }
@@ -149,14 +178,14 @@ std::string FromV8String(v8::Local<v8::String> val) {
149
178
  }
150
179
 
151
180
  void Exec(const Nan::FunctionCallbackInfo<v8::Value>& info) {
152
- v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext();
153
-
154
181
  if (info.Length() < 2) {
155
182
  Nan::ThrowTypeError("Wrong number of arguments");
183
+ return;
156
184
  }
157
185
 
158
186
  if (!info[0]->IsString() || !info[1]->IsString()) {
159
187
  Nan::ThrowTypeError("Wrong arguments");
188
+ return;
160
189
  }
161
190
 
162
191
  std::string json = FromV8String(Nan::To<v8::String>(info[0]).ToLocalChecked());
@@ -166,12 +195,10 @@ void Exec(const Nan::FunctionCallbackInfo<v8::Value>& info) {
166
195
  }
167
196
 
168
197
  void Init(v8::Local<v8::Object> exports) {
169
- v8::Local<v8::Context> context = exports->CreationContext();
170
- exports->Set(context,
171
- Nan::New("exec").ToLocalChecked(),
172
- Nan::New<v8::FunctionTemplate>(Exec)
173
- ->GetFunction(context)
174
- .ToLocalChecked());
198
+ v8::Local<v8::Context> context = exports->GetCreationContext().ToLocalChecked();
199
+ (void)exports->Set(context,
200
+ Nan::New("exec").ToLocalChecked(),
201
+ Nan::New<v8::FunctionTemplate>(Exec)->GetFunction(context).ToLocalChecked());
175
202
  }
176
203
 
177
204
  NODE_MODULE(exec, Init)
@@ -3,7 +3,7 @@ const jq = require('../lib');
3
3
  describe('jq', () => {
4
4
  it('should break', () => {
5
5
  const json = { foo2: 'bar' };
6
- const input = '.foo';
6
+ const input = 'foo';
7
7
  const result = jq.exec(json, input);
8
8
 
9
9
  expect(result).toBe(null);
@@ -116,7 +116,7 @@ describe('jq', () => {
116
116
  const input = '.foo';
117
117
  const result = jq.exec(json, input);
118
118
 
119
- expect(result).toBe(undefined);
119
+ expect(result).toBe(null);
120
120
  })
121
121
 
122
122
  it('should excape \'\' to ""', () => {
@@ -157,5 +157,15 @@ describe('jq', () => {
157
157
  expect(jq.exec({}, 'env', {})).toEqual({});
158
158
  expect(jq.exec({}, 'env')).toEqual({});
159
159
  })
160
+
161
+ it('test throw on error', () => {
162
+ expect(() => { jq.exec({}, 'foo', {throwOnError: true}) }).toThrow("jq: error: foo/0 is not defined at <top-level>, line 1:");
163
+ expect(() => { jq.exec({}, '1/0', {throwOnError: true}) }).toThrow("jq: error: Division by zero? at <top-level>, line 1:");
164
+ expect(() => { jq.exec({}, '{', {throwOnError: true}) }).toThrow("jq: error: syntax error, unexpected $end (Unix shell quoting issues?) at <top-level>, line 1:");
165
+ expect(() => { jq.exec({}, '{(0):1}', {throwOnError: true}) }).toThrow("jq: error: Cannot use number (0) as object key at <top-level>, line 1:");
166
+ expect(() => { jq.exec({}, 'if true then 1 else 0', {throwOnError: true}) }).toThrow("jq: error: Possibly unterminated 'if' statement at <top-level>, line 1:");
167
+ expect(() => { jq.exec({}, 'null | map(.+1)', {throwOnError: true}) }).toThrow("jq: error: Cannot iterate over null (null)");
168
+ expect(() => { jq.exec({foo: "bar"}, '.foo + 1', {throwOnError: true}) }).toThrow("jq: error: string (\"bar\") and number (1) cannot be added");
169
+ })
160
170
  })
161
171
 
@@ -45,7 +45,7 @@ describe('template', () => {
45
45
  const input = '{{.foo}}';
46
46
  const result = jq.renderRecursively(json, input);
47
47
 
48
- expect(result).toBe(undefined);
48
+ expect(result).toBe(null);
49
49
  });
50
50
  it('should excape \'\' to ""', () => {
51
51
  const json = { foo: 'com' };
@@ -136,5 +136,35 @@ describe('template', () => {
136
136
  expect(render([{'{{.bar}}': [false, '/foo/{{.foo + .bar}}']}])).toEqual([{foo: [false, '/foo/barfoo']}]);
137
137
  expect(render({foo: [{bar: '{{1}}'}, '{{empty}}']})).toEqual({foo: [{bar: 1}, undefined]});
138
138
  });
139
+ it('should accept quotes outside of template', () => {
140
+ const json = { foo: 'bar', bar: 'foo' };
141
+ const render = (input) => jq.renderRecursively(json, input);
142
+
143
+ expect(render('"{{.foo}}"')).toEqual('"bar"');
144
+ expect(render('\'{{.foo}}\'')).toEqual('\'bar\'');
145
+ });
146
+ it('should accept escaped quotes inside jq template', () => {
147
+ const json = { foo: 'bar', bar: 'foo' };
148
+ const render = (input) => jq.renderRecursively(json, input);
149
+
150
+ expect(render('{{"\\"foo\\""}}')).toEqual('"foo"');
151
+ });
152
+ it('test disable env', () => {
153
+ expect(jq.renderRecursively({}, '{{env}}', {enableEnv: false})).toEqual({});
154
+ expect(jq.renderRecursively({}, '{{env}}', {enableEnv: true})).not.toEqual({});
155
+ expect(jq.renderRecursively({}, '{{env}}', {})).toEqual({});
156
+ expect(jq.renderRecursively({}, '{{env}}')).toEqual({});
157
+ })
158
+ it('test throw on error', () => {
159
+ expect(() => { jq.renderRecursively({}, '{{foo}}', {throwOnError: true}) }).toThrow("jq: error: foo/0 is not defined at <top-level>, line 1:");
160
+ expect(() => { jq.renderRecursively({}, '{{1/0}}', {throwOnError: true}) }).toThrow("jq: error: Division by zero? at <top-level>, line 1:");
161
+ expect(() => { jq.renderRecursively({}, '{{{}}', {throwOnError: true}) }).toThrow("jq: error: syntax error, unexpected $end (Unix shell quoting issues?) at <top-level>, line 1:");
162
+ expect(() => { jq.renderRecursively({}, '{{ {(0):1} }}', {throwOnError: true}) }).toThrow("jq: error: Cannot use number (0) as object key at <top-level>, line 1:");
163
+ expect(() => { jq.renderRecursively({}, '{{if true then 1 else 0}}', {throwOnError: true}) }).toThrow("jq: error: Possibly unterminated 'if' statement at <top-level>, line 1:");
164
+ expect(() => { jq.renderRecursively({}, '{{null | map(.+1)}}', {throwOnError: true}) }).toThrow("jq: error: Cannot iterate over null (null)");
165
+ expect(() => { jq.renderRecursively({foo: "bar"}, '{{.foo + 1}}', {throwOnError: true}) }).toThrow("jq: error: string (\"bar\") and number (1) cannot be added");
166
+ expect(() => { jq.renderRecursively({}, '{{foo}}/{{bar}}', {throwOnError: true}) }).toThrow("jq: error: foo/0 is not defined at <top-level>, line 1:");
167
+ expect(() => { jq.renderRecursively({}, '/{{foo}}/', {throwOnError: true}) }).toThrow("jq: error: foo/0 is not defined at <top-level>, line 1:");
168
+ })
139
169
  })
140
170