@port-labs/jq-node-bindings 0.0.10 → 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 +5 -2
- package/lib/jq.js +7 -4
- package/lib/template.js +15 -15
- package/package.json +2 -2
- package/src/binding.cc +45 -18
- package/test/santiy.test.js +12 -2
- package/test/template.test.js +18 -1
package/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
declare module '@port-labs/jq-node-bindings' {
|
|
2
|
-
|
|
3
|
-
|
|
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,
|
|
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
|
|
7
|
+
return enableEnv ? formattedFilter : `def env: {}; {} as $ENV | ${formattedFilter}`;
|
|
8
8
|
}
|
|
9
|
-
const exec = (object, filter,
|
|
9
|
+
const exec = (object, filter, {enableEnv = false, throwOnError = false} = {}) => {
|
|
10
10
|
try {
|
|
11
|
-
const data = nativeJq.exec(JSON.stringify(object), formatFilter(filter,
|
|
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
|
@@ -51,7 +51,7 @@ const findInsideDoubleBracesIndices = (input) => {
|
|
|
51
51
|
return indices;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const render = (inputJson, template) => {
|
|
54
|
+
const render = (inputJson, template, execOptions = {}) => {
|
|
55
55
|
if (typeof template !== 'string') {
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
@@ -64,12 +64,12 @@ const render = (inputJson, template) => {
|
|
|
64
64
|
const firstIndex = indices[0];
|
|
65
65
|
if (indices.length === 1 && template.trim().startsWith('{{') && template.trim().endsWith('}}')) {
|
|
66
66
|
// If entire string is a template, evaluate and return the result with the original type
|
|
67
|
-
return jq.exec(inputJson, template.slice(firstIndex.start, firstIndex.end));
|
|
67
|
+
return jq.exec(inputJson, template.slice(firstIndex.start, firstIndex.end), execOptions);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
let result = template.slice(0, firstIndex.start - '{{'.length); // Initiate result with string until first template start index
|
|
71
71
|
indices.forEach((index, i) => {
|
|
72
|
-
const jqResult = jq.exec(inputJson, template.slice(index.start, index.end));
|
|
72
|
+
const jqResult = jq.exec(inputJson, template.slice(index.start, index.end), execOptions);
|
|
73
73
|
result +=
|
|
74
74
|
// Add to the result the stringified evaluated jq of the current template
|
|
75
75
|
(typeof jqResult === 'string' ? jqResult : JSON.stringify(jqResult)) +
|
|
@@ -83,24 +83,24 @@ const render = (inputJson, template) => {
|
|
|
83
83
|
return result;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
const renderRecursively = (inputJson, template) => {
|
|
86
|
+
const renderRecursively = (inputJson, template, execOptions = {}) => {
|
|
87
87
|
if (typeof template === 'string') {
|
|
88
|
-
return render(inputJson, template);
|
|
88
|
+
return render(inputJson, template, execOptions);
|
|
89
89
|
}
|
|
90
90
|
if (Array.isArray(template)) {
|
|
91
|
-
return template.map((value) => renderRecursively(inputJson, value));
|
|
91
|
+
return template.map((value) => renderRecursively(inputJson, value, execOptions));
|
|
92
92
|
}
|
|
93
93
|
if (typeof template === 'object' && template !== null) {
|
|
94
94
|
return Object.fromEntries(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
}),
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
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.11",
|
|
4
4
|
"description": "Node.js bindings for JQ",
|
|
5
|
-
"jq-node-bindings": "0.0.
|
|
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
|
-
|
|
146
|
+
Nan::ThrowTypeError(err_msg.buf);
|
|
116
147
|
return;
|
|
117
148
|
}
|
|
118
149
|
cache.put(filter, jq);
|
|
119
150
|
}
|
|
120
151
|
|
|
121
|
-
|
|
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
|
-
|
|
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->
|
|
170
|
-
exports->Set(context,
|
|
171
|
-
|
|
172
|
-
|
|
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)
|
package/test/santiy.test.js
CHANGED
|
@@ -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 = '
|
|
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(
|
|
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
|
|
package/test/template.test.js
CHANGED
|
@@ -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(
|
|
48
|
+
expect(result).toBe(null);
|
|
49
49
|
});
|
|
50
50
|
it('should excape \'\' to ""', () => {
|
|
51
51
|
const json = { foo: 'com' };
|
|
@@ -149,5 +149,22 @@ describe('template', () => {
|
|
|
149
149
|
|
|
150
150
|
expect(render('{{"\\"foo\\""}}')).toEqual('"foo"');
|
|
151
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
|
+
})
|
|
152
169
|
})
|
|
153
170
|
|