testblocks 0.1.0
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/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/cli/executor.d.ts +32 -0
- package/dist/cli/executor.js +517 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +411 -0
- package/dist/cli/reporters.d.ts +62 -0
- package/dist/cli/reporters.js +451 -0
- package/dist/client/assets/index-4hbFPUhP.js +2087 -0
- package/dist/client/assets/index-4hbFPUhP.js.map +1 -0
- package/dist/client/assets/index-Dnk1ti7l.css +1 -0
- package/dist/client/index.html +25 -0
- package/dist/core/blocks/api.d.ts +2 -0
- package/dist/core/blocks/api.js +610 -0
- package/dist/core/blocks/data-driven.d.ts +2 -0
- package/dist/core/blocks/data-driven.js +245 -0
- package/dist/core/blocks/index.d.ts +15 -0
- package/dist/core/blocks/index.js +71 -0
- package/dist/core/blocks/lifecycle.d.ts +2 -0
- package/dist/core/blocks/lifecycle.js +199 -0
- package/dist/core/blocks/logic.d.ts +2 -0
- package/dist/core/blocks/logic.js +357 -0
- package/dist/core/blocks/playwright.d.ts +2 -0
- package/dist/core/blocks/playwright.js +764 -0
- package/dist/core/blocks/procedures.d.ts +5 -0
- package/dist/core/blocks/procedures.js +321 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +44 -0
- package/dist/core/plugins.d.ts +66 -0
- package/dist/core/plugins.js +118 -0
- package/dist/core/types.d.ts +153 -0
- package/dist/core/types.js +2 -0
- package/dist/server/codegenManager.d.ts +54 -0
- package/dist/server/codegenManager.js +259 -0
- package/dist/server/codegenParser.d.ts +17 -0
- package/dist/server/codegenParser.js +598 -0
- package/dist/server/executor.d.ts +37 -0
- package/dist/server/executor.js +672 -0
- package/dist/server/globals.d.ts +85 -0
- package/dist/server/globals.js +273 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +361 -0
- package/dist/server/plugins.d.ts +55 -0
- package/dist/server/plugins.js +206 -0
- package/package.json +103 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.apiBlocks = void 0;
|
|
7
|
+
const jsonpath_plus_1 = require("jsonpath-plus");
|
|
8
|
+
const xpath_1 = __importDefault(require("xpath"));
|
|
9
|
+
const xmldom_1 = require("xmldom");
|
|
10
|
+
// API Testing Blocks
|
|
11
|
+
exports.apiBlocks = [
|
|
12
|
+
// ============================================
|
|
13
|
+
// HEADER MANAGEMENT BLOCKS
|
|
14
|
+
// ============================================
|
|
15
|
+
// Set a single header
|
|
16
|
+
{
|
|
17
|
+
type: 'api_set_header',
|
|
18
|
+
category: 'API',
|
|
19
|
+
color: '#7B1FA2',
|
|
20
|
+
tooltip: 'Set a request header (applies to subsequent requests)',
|
|
21
|
+
inputs: [
|
|
22
|
+
{ name: 'NAME', type: 'field', fieldType: 'text', required: true, default: 'Authorization' },
|
|
23
|
+
{ name: 'VALUE', type: 'field', fieldType: 'text', required: true, default: 'Bearer token' },
|
|
24
|
+
],
|
|
25
|
+
previousStatement: true,
|
|
26
|
+
nextStatement: true,
|
|
27
|
+
execute: async (params, context) => {
|
|
28
|
+
const name = params.NAME;
|
|
29
|
+
const value = resolveVariables(params.VALUE, context);
|
|
30
|
+
// Get or create headers map
|
|
31
|
+
let headers = context.variables.get('__requestHeaders');
|
|
32
|
+
if (!headers) {
|
|
33
|
+
headers = {};
|
|
34
|
+
}
|
|
35
|
+
headers[name] = value;
|
|
36
|
+
context.variables.set('__requestHeaders', headers);
|
|
37
|
+
context.logger.info(`Set header: ${name}: ${value.substring(0, 30)}${value.length > 30 ? '...' : ''}`);
|
|
38
|
+
return {
|
|
39
|
+
_summary: `${name}: ${value.substring(0, 30)}${value.length > 30 ? '...' : ''}`,
|
|
40
|
+
name,
|
|
41
|
+
value,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
// Set multiple headers at once
|
|
46
|
+
{
|
|
47
|
+
type: 'api_set_headers',
|
|
48
|
+
category: 'API',
|
|
49
|
+
color: '#7B1FA2',
|
|
50
|
+
tooltip: 'Set multiple request headers from JSON object',
|
|
51
|
+
inputs: [
|
|
52
|
+
{ name: 'HEADERS', type: 'field', fieldType: 'text', required: true, default: '{"Content-Type": "application/json"}' },
|
|
53
|
+
],
|
|
54
|
+
previousStatement: true,
|
|
55
|
+
nextStatement: true,
|
|
56
|
+
execute: async (params, context) => {
|
|
57
|
+
const headersStr = resolveVariables(params.HEADERS, context);
|
|
58
|
+
try {
|
|
59
|
+
const newHeaders = JSON.parse(headersStr);
|
|
60
|
+
// Get or create headers map
|
|
61
|
+
let headers = context.variables.get('__requestHeaders');
|
|
62
|
+
if (!headers) {
|
|
63
|
+
headers = {};
|
|
64
|
+
}
|
|
65
|
+
// Merge new headers
|
|
66
|
+
Object.assign(headers, newHeaders);
|
|
67
|
+
context.variables.set('__requestHeaders', headers);
|
|
68
|
+
const headerNames = Object.keys(newHeaders).join(', ');
|
|
69
|
+
context.logger.info(`Set headers: ${headerNames}`);
|
|
70
|
+
return {
|
|
71
|
+
_summary: `Set ${Object.keys(newHeaders).length} headers`,
|
|
72
|
+
headers: newHeaders,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new Error(`Invalid JSON for headers: ${headersStr}`);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
// Clear all headers
|
|
81
|
+
{
|
|
82
|
+
type: 'api_clear_headers',
|
|
83
|
+
category: 'API',
|
|
84
|
+
color: '#7B1FA2',
|
|
85
|
+
tooltip: 'Clear all request headers',
|
|
86
|
+
inputs: [],
|
|
87
|
+
previousStatement: true,
|
|
88
|
+
nextStatement: true,
|
|
89
|
+
execute: async (params, context) => {
|
|
90
|
+
context.variables.delete('__requestHeaders');
|
|
91
|
+
context.logger.info('Cleared all request headers');
|
|
92
|
+
return { _summary: 'Headers cleared' };
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
// ============================================
|
|
96
|
+
// STATEMENT BLOCKS - Chain together visually
|
|
97
|
+
// ============================================
|
|
98
|
+
// HTTP GET Request (Statement - stores response)
|
|
99
|
+
{
|
|
100
|
+
type: 'api_get',
|
|
101
|
+
category: 'API',
|
|
102
|
+
color: '#4CAF50',
|
|
103
|
+
tooltip: 'Perform HTTP GET request and store response',
|
|
104
|
+
inputs: [
|
|
105
|
+
{ name: 'URL', type: 'field', fieldType: 'text', required: true },
|
|
106
|
+
{ name: 'HEADERS', type: 'field', fieldType: 'text', default: '' },
|
|
107
|
+
],
|
|
108
|
+
previousStatement: true,
|
|
109
|
+
nextStatement: true,
|
|
110
|
+
execute: async (params, context) => {
|
|
111
|
+
const url = resolveVariables(params.URL, context);
|
|
112
|
+
const contextHeaders = context.variables.get('__requestHeaders') || {};
|
|
113
|
+
const inlineHeaders = parseHeaders(params.HEADERS, context);
|
|
114
|
+
const headers = { ...contextHeaders, ...inlineHeaders };
|
|
115
|
+
context.logger.info(`GET ${url}`);
|
|
116
|
+
const response = await fetch(url, {
|
|
117
|
+
headers,
|
|
118
|
+
signal: context.abortSignal,
|
|
119
|
+
});
|
|
120
|
+
const parsed = await parseResponse(response);
|
|
121
|
+
context.variables.set('__lastResponse', parsed);
|
|
122
|
+
return parsed;
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
// HTTP POST Request (Statement)
|
|
126
|
+
{
|
|
127
|
+
type: 'api_post',
|
|
128
|
+
category: 'API',
|
|
129
|
+
color: '#4CAF50',
|
|
130
|
+
tooltip: 'Perform HTTP POST request and store response',
|
|
131
|
+
inputs: [
|
|
132
|
+
{ name: 'URL', type: 'field', fieldType: 'text', required: true },
|
|
133
|
+
{ name: 'BODY', type: 'field', fieldType: 'text', default: '{}' },
|
|
134
|
+
{ name: 'HEADERS', type: 'field', fieldType: 'text', default: '' },
|
|
135
|
+
],
|
|
136
|
+
previousStatement: true,
|
|
137
|
+
nextStatement: true,
|
|
138
|
+
execute: async (params, context) => {
|
|
139
|
+
const url = resolveVariables(params.URL, context);
|
|
140
|
+
const bodyStr = resolveVariables(params.BODY || '{}', context);
|
|
141
|
+
const contextHeaders = context.variables.get('__requestHeaders') || {};
|
|
142
|
+
const inlineHeaders = parseHeaders(params.HEADERS, context);
|
|
143
|
+
let body;
|
|
144
|
+
try {
|
|
145
|
+
body = JSON.parse(bodyStr);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
body = bodyStr;
|
|
149
|
+
}
|
|
150
|
+
context.logger.info(`POST ${url}`);
|
|
151
|
+
const response = await fetch(url, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json', ...contextHeaders, ...inlineHeaders },
|
|
154
|
+
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
155
|
+
signal: context.abortSignal,
|
|
156
|
+
});
|
|
157
|
+
const parsed = await parseResponse(response);
|
|
158
|
+
context.variables.set('__lastResponse', parsed);
|
|
159
|
+
return parsed;
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
// HTTP PUT Request (Statement)
|
|
163
|
+
{
|
|
164
|
+
type: 'api_put',
|
|
165
|
+
category: 'API',
|
|
166
|
+
color: '#4CAF50',
|
|
167
|
+
tooltip: 'Perform HTTP PUT request and store response',
|
|
168
|
+
inputs: [
|
|
169
|
+
{ name: 'URL', type: 'field', fieldType: 'text', required: true },
|
|
170
|
+
{ name: 'BODY', type: 'field', fieldType: 'text', default: '{}' },
|
|
171
|
+
{ name: 'HEADERS', type: 'field', fieldType: 'text', default: '' },
|
|
172
|
+
],
|
|
173
|
+
previousStatement: true,
|
|
174
|
+
nextStatement: true,
|
|
175
|
+
execute: async (params, context) => {
|
|
176
|
+
const url = resolveVariables(params.URL, context);
|
|
177
|
+
const bodyStr = resolveVariables(params.BODY || '{}', context);
|
|
178
|
+
const contextHeaders = context.variables.get('__requestHeaders') || {};
|
|
179
|
+
const inlineHeaders = parseHeaders(params.HEADERS, context);
|
|
180
|
+
let body;
|
|
181
|
+
try {
|
|
182
|
+
body = JSON.parse(bodyStr);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
body = bodyStr;
|
|
186
|
+
}
|
|
187
|
+
context.logger.info(`PUT ${url}`);
|
|
188
|
+
const response = await fetch(url, {
|
|
189
|
+
method: 'PUT',
|
|
190
|
+
headers: { 'Content-Type': 'application/json', ...contextHeaders, ...inlineHeaders },
|
|
191
|
+
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
192
|
+
signal: context.abortSignal,
|
|
193
|
+
});
|
|
194
|
+
const parsed = await parseResponse(response);
|
|
195
|
+
context.variables.set('__lastResponse', parsed);
|
|
196
|
+
return parsed;
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
// HTTP PATCH Request (Statement)
|
|
200
|
+
{
|
|
201
|
+
type: 'api_patch',
|
|
202
|
+
category: 'API',
|
|
203
|
+
color: '#4CAF50',
|
|
204
|
+
tooltip: 'Perform HTTP PATCH request and store response',
|
|
205
|
+
inputs: [
|
|
206
|
+
{ name: 'URL', type: 'field', fieldType: 'text', required: true },
|
|
207
|
+
{ name: 'BODY', type: 'field', fieldType: 'text', default: '{}' },
|
|
208
|
+
{ name: 'HEADERS', type: 'field', fieldType: 'text', default: '' },
|
|
209
|
+
],
|
|
210
|
+
previousStatement: true,
|
|
211
|
+
nextStatement: true,
|
|
212
|
+
execute: async (params, context) => {
|
|
213
|
+
const url = resolveVariables(params.URL, context);
|
|
214
|
+
const bodyStr = resolveVariables(params.BODY || '{}', context);
|
|
215
|
+
const contextHeaders = context.variables.get('__requestHeaders') || {};
|
|
216
|
+
const inlineHeaders = parseHeaders(params.HEADERS, context);
|
|
217
|
+
let body;
|
|
218
|
+
try {
|
|
219
|
+
body = JSON.parse(bodyStr);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
body = bodyStr;
|
|
223
|
+
}
|
|
224
|
+
context.logger.info(`PATCH ${url}`);
|
|
225
|
+
const response = await fetch(url, {
|
|
226
|
+
method: 'PATCH',
|
|
227
|
+
headers: { 'Content-Type': 'application/json', ...contextHeaders, ...inlineHeaders },
|
|
228
|
+
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
229
|
+
signal: context.abortSignal,
|
|
230
|
+
});
|
|
231
|
+
const parsed = await parseResponse(response);
|
|
232
|
+
context.variables.set('__lastResponse', parsed);
|
|
233
|
+
return parsed;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
// HTTP DELETE Request (Statement)
|
|
237
|
+
{
|
|
238
|
+
type: 'api_delete',
|
|
239
|
+
category: 'API',
|
|
240
|
+
color: '#4CAF50',
|
|
241
|
+
tooltip: 'Perform HTTP DELETE request and store response',
|
|
242
|
+
inputs: [
|
|
243
|
+
{ name: 'URL', type: 'field', fieldType: 'text', required: true },
|
|
244
|
+
{ name: 'HEADERS', type: 'field', fieldType: 'text', default: '' },
|
|
245
|
+
],
|
|
246
|
+
previousStatement: true,
|
|
247
|
+
nextStatement: true,
|
|
248
|
+
execute: async (params, context) => {
|
|
249
|
+
const url = resolveVariables(params.URL, context);
|
|
250
|
+
const contextHeaders = context.variables.get('__requestHeaders') || {};
|
|
251
|
+
const inlineHeaders = parseHeaders(params.HEADERS, context);
|
|
252
|
+
const headers = { ...contextHeaders, ...inlineHeaders };
|
|
253
|
+
context.logger.info(`DELETE ${url}`);
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
method: 'DELETE',
|
|
256
|
+
headers,
|
|
257
|
+
signal: context.abortSignal,
|
|
258
|
+
});
|
|
259
|
+
const parsed = await parseResponse(response);
|
|
260
|
+
context.variables.set('__lastResponse', parsed);
|
|
261
|
+
return parsed;
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
// Assert Status Code (uses last response if not provided)
|
|
265
|
+
{
|
|
266
|
+
type: 'api_assert_status',
|
|
267
|
+
category: 'API',
|
|
268
|
+
color: '#FF9800',
|
|
269
|
+
tooltip: 'Assert that response has expected status code',
|
|
270
|
+
inputs: [
|
|
271
|
+
{ name: 'STATUS', type: 'field', fieldType: 'number', default: 200, required: true },
|
|
272
|
+
],
|
|
273
|
+
previousStatement: true,
|
|
274
|
+
nextStatement: true,
|
|
275
|
+
execute: async (params, context) => {
|
|
276
|
+
const response = context.variables.get('__lastResponse');
|
|
277
|
+
if (!response) {
|
|
278
|
+
throw new Error('No response available. Make sure to call an API request first.');
|
|
279
|
+
}
|
|
280
|
+
const expectedStatus = params.STATUS;
|
|
281
|
+
if (response.status !== expectedStatus) {
|
|
282
|
+
throw new Error(`Expected status ${expectedStatus} but got ${response.status}`);
|
|
283
|
+
}
|
|
284
|
+
context.logger.info(`✓ Status is ${expectedStatus}`);
|
|
285
|
+
return {
|
|
286
|
+
_summary: `✓ Status ${response.status} === ${expectedStatus}`,
|
|
287
|
+
expected: expectedStatus,
|
|
288
|
+
actual: response.status,
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
// Assert Response Body Contains
|
|
293
|
+
{
|
|
294
|
+
type: 'api_assert_body_contains',
|
|
295
|
+
category: 'API',
|
|
296
|
+
color: '#FF9800',
|
|
297
|
+
tooltip: 'Assert that response body contains expected value',
|
|
298
|
+
inputs: [
|
|
299
|
+
{ name: 'PATH', type: 'field', fieldType: 'text', default: '' },
|
|
300
|
+
{ name: 'VALUE', type: 'field', fieldType: 'text', required: true },
|
|
301
|
+
],
|
|
302
|
+
previousStatement: true,
|
|
303
|
+
nextStatement: true,
|
|
304
|
+
execute: async (params, context) => {
|
|
305
|
+
const response = context.variables.get('__lastResponse');
|
|
306
|
+
if (!response) {
|
|
307
|
+
throw new Error('No response available. Make sure to call an API request first.');
|
|
308
|
+
}
|
|
309
|
+
const path = params.PATH;
|
|
310
|
+
const expectedValue = resolveVariables(params.VALUE, context);
|
|
311
|
+
const actualValue = path ? getValueByPath(response.body, path) : response.body;
|
|
312
|
+
const actualStr = typeof actualValue === 'string' ? actualValue : JSON.stringify(actualValue);
|
|
313
|
+
if (!actualStr.includes(expectedValue)) {
|
|
314
|
+
throw new Error(`Expected ${path || 'body'} to contain "${expectedValue}" but got "${actualStr}"`);
|
|
315
|
+
}
|
|
316
|
+
context.logger.info(`✓ ${path || 'body'} contains "${expectedValue}"`);
|
|
317
|
+
return {
|
|
318
|
+
_summary: `✓ ${path || 'body'} contains "${expectedValue}"`,
|
|
319
|
+
path: path || 'body',
|
|
320
|
+
expected: expectedValue,
|
|
321
|
+
actual: actualStr.substring(0, 100) + (actualStr.length > 100 ? '...' : ''),
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
// Extract Value using JSONPath
|
|
326
|
+
{
|
|
327
|
+
type: 'api_extract_jsonpath',
|
|
328
|
+
category: 'API',
|
|
329
|
+
color: '#2196F3',
|
|
330
|
+
tooltip: 'Extract a value from JSON response using JSONPath expression',
|
|
331
|
+
inputs: [
|
|
332
|
+
{ name: 'JSONPATH', type: 'field', fieldType: 'text', required: true, default: '$.data.id' },
|
|
333
|
+
{ name: 'VARIABLE', type: 'field', fieldType: 'text', required: true },
|
|
334
|
+
],
|
|
335
|
+
previousStatement: true,
|
|
336
|
+
nextStatement: true,
|
|
337
|
+
execute: async (params, context) => {
|
|
338
|
+
const response = context.variables.get('__lastResponse');
|
|
339
|
+
if (!response) {
|
|
340
|
+
throw new Error('No response available. Make sure to call an API request first.');
|
|
341
|
+
}
|
|
342
|
+
const jsonPath = params.JSONPATH;
|
|
343
|
+
const varName = params.VARIABLE;
|
|
344
|
+
try {
|
|
345
|
+
const results = (0, jsonpath_plus_1.JSONPath)({ path: jsonPath, json: response.body });
|
|
346
|
+
// If single result, unwrap from array
|
|
347
|
+
const value = results.length === 1 ? results[0] : results;
|
|
348
|
+
context.variables.set(varName, value);
|
|
349
|
+
const valueStr = JSON.stringify(value);
|
|
350
|
+
context.logger.info(`Extracted (JSONPath) ${jsonPath} → ${varName} = ${valueStr}`);
|
|
351
|
+
return {
|
|
352
|
+
_summary: `${varName} = ${valueStr.substring(0, 50)}${valueStr.length > 50 ? '...' : ''}`,
|
|
353
|
+
variable: varName,
|
|
354
|
+
jsonPath,
|
|
355
|
+
value,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
throw new Error(`Invalid JSONPath expression: ${jsonPath}. Error: ${e.message}`);
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
// Extract Value using XPath (for XML/HTML responses)
|
|
364
|
+
{
|
|
365
|
+
type: 'api_extract_xpath',
|
|
366
|
+
category: 'API',
|
|
367
|
+
color: '#2196F3',
|
|
368
|
+
tooltip: 'Extract a value from XML/HTML response using XPath expression',
|
|
369
|
+
inputs: [
|
|
370
|
+
{ name: 'XPATH', type: 'field', fieldType: 'text', required: true, default: '//title/text()' },
|
|
371
|
+
{ name: 'VARIABLE', type: 'field', fieldType: 'text', required: true },
|
|
372
|
+
],
|
|
373
|
+
previousStatement: true,
|
|
374
|
+
nextStatement: true,
|
|
375
|
+
execute: async (params, context) => {
|
|
376
|
+
const response = context.variables.get('__lastResponse');
|
|
377
|
+
if (!response) {
|
|
378
|
+
throw new Error('No response available. Make sure to call an API request first.');
|
|
379
|
+
}
|
|
380
|
+
const xpathExpr = params.XPATH;
|
|
381
|
+
const varName = params.VARIABLE;
|
|
382
|
+
try {
|
|
383
|
+
// Ensure body is a string for XML parsing
|
|
384
|
+
const xmlString = typeof response.body === 'string'
|
|
385
|
+
? response.body
|
|
386
|
+
: JSON.stringify(response.body);
|
|
387
|
+
const doc = new xmldom_1.DOMParser().parseFromString(xmlString, 'text/xml');
|
|
388
|
+
const nodes = xpath_1.default.select(xpathExpr, doc);
|
|
389
|
+
// Convert nodes to values
|
|
390
|
+
let value;
|
|
391
|
+
if (Array.isArray(nodes)) {
|
|
392
|
+
if (nodes.length === 0) {
|
|
393
|
+
value = null;
|
|
394
|
+
}
|
|
395
|
+
else if (nodes.length === 1) {
|
|
396
|
+
value = getNodeValue(nodes[0]);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
value = nodes.map(getNodeValue);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
value = nodes;
|
|
404
|
+
}
|
|
405
|
+
context.variables.set(varName, value);
|
|
406
|
+
const valueStr = JSON.stringify(value);
|
|
407
|
+
context.logger.info(`Extracted (XPath) ${xpathExpr} → ${varName} = ${valueStr}`);
|
|
408
|
+
return {
|
|
409
|
+
_summary: `${varName} = ${valueStr.substring(0, 50)}${valueStr.length > 50 ? '...' : ''}`,
|
|
410
|
+
variable: varName,
|
|
411
|
+
xpath: xpathExpr,
|
|
412
|
+
value,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
throw new Error(`XPath extraction failed: ${xpathExpr}. Error: ${e.message}`);
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
// Legacy extract (simple dot notation) - kept for backwards compatibility
|
|
421
|
+
{
|
|
422
|
+
type: 'api_extract',
|
|
423
|
+
category: 'API',
|
|
424
|
+
color: '#2196F3',
|
|
425
|
+
tooltip: 'Extract a value from response using dot notation (e.g., data.user.name)',
|
|
426
|
+
inputs: [
|
|
427
|
+
{ name: 'PATH', type: 'field', fieldType: 'text', required: true },
|
|
428
|
+
{ name: 'VARIABLE', type: 'field', fieldType: 'text', required: true },
|
|
429
|
+
],
|
|
430
|
+
previousStatement: true,
|
|
431
|
+
nextStatement: true,
|
|
432
|
+
execute: async (params, context) => {
|
|
433
|
+
const response = context.variables.get('__lastResponse');
|
|
434
|
+
if (!response) {
|
|
435
|
+
throw new Error('No response available. Make sure to call an API request first.');
|
|
436
|
+
}
|
|
437
|
+
const path = params.PATH;
|
|
438
|
+
const varName = params.VARIABLE;
|
|
439
|
+
const value = getValueByPath(response.body, path);
|
|
440
|
+
context.variables.set(varName, value);
|
|
441
|
+
const valueStr = JSON.stringify(value);
|
|
442
|
+
context.logger.info(`Extracted ${path} → ${varName} = ${valueStr}`);
|
|
443
|
+
return {
|
|
444
|
+
_summary: `${varName} = ${valueStr.substring(0, 50)}${valueStr.length > 50 ? '...' : ''}`,
|
|
445
|
+
variable: varName,
|
|
446
|
+
path,
|
|
447
|
+
value,
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
// ============================================
|
|
452
|
+
// VALUE BLOCKS - For advanced compositions
|
|
453
|
+
// ============================================
|
|
454
|
+
// Create Headers Object
|
|
455
|
+
{
|
|
456
|
+
type: 'api_headers',
|
|
457
|
+
category: 'API',
|
|
458
|
+
color: '#9C27B0',
|
|
459
|
+
tooltip: 'Create a headers object',
|
|
460
|
+
inputs: [
|
|
461
|
+
{ name: 'AUTH_TYPE', type: 'field', fieldType: 'dropdown', options: [['None', 'none'], ['Bearer Token', 'bearer'], ['Basic Auth', 'basic'], ['API Key', 'apikey']] },
|
|
462
|
+
{ name: 'AUTH_VALUE', type: 'field', fieldType: 'text' },
|
|
463
|
+
{ name: 'CUSTOM', type: 'value', check: 'Object' },
|
|
464
|
+
],
|
|
465
|
+
output: { type: 'Object' },
|
|
466
|
+
execute: async (params, context) => {
|
|
467
|
+
const authType = params.AUTH_TYPE;
|
|
468
|
+
const authValue = resolveVariables(params.AUTH_VALUE || '', context);
|
|
469
|
+
const custom = params.CUSTOM || {};
|
|
470
|
+
const headers = { ...custom };
|
|
471
|
+
switch (authType) {
|
|
472
|
+
case 'bearer':
|
|
473
|
+
headers['Authorization'] = `Bearer ${authValue}`;
|
|
474
|
+
break;
|
|
475
|
+
case 'basic':
|
|
476
|
+
headers['Authorization'] = `Basic ${Buffer.from(authValue).toString('base64')}`;
|
|
477
|
+
break;
|
|
478
|
+
case 'apikey':
|
|
479
|
+
headers['X-API-Key'] = authValue;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
return headers;
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
// Create JSON Body
|
|
486
|
+
{
|
|
487
|
+
type: 'api_json_body',
|
|
488
|
+
category: 'API',
|
|
489
|
+
color: '#9C27B0',
|
|
490
|
+
tooltip: 'Create a JSON body from key-value pairs or raw JSON',
|
|
491
|
+
inputs: [
|
|
492
|
+
{ name: 'JSON', type: 'field', fieldType: 'text', default: '{}' },
|
|
493
|
+
],
|
|
494
|
+
output: { type: 'Object' },
|
|
495
|
+
execute: async (params, context) => {
|
|
496
|
+
const json = resolveVariables(params.JSON, context);
|
|
497
|
+
return JSON.parse(json);
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
];
|
|
501
|
+
// Helper functions
|
|
502
|
+
function resolveVariables(text, context) {
|
|
503
|
+
// Match ${varName} or ${varName.property.path}
|
|
504
|
+
return text.replace(/\$\{([\w.]+)\}/g, (match, path) => {
|
|
505
|
+
const parts = path.split('.');
|
|
506
|
+
const varName = parts[0];
|
|
507
|
+
let value = context.variables.get(varName);
|
|
508
|
+
// Navigate through object properties if path has dots
|
|
509
|
+
if (parts.length > 1 && value !== undefined && value !== null) {
|
|
510
|
+
for (let i = 1; i < parts.length; i++) {
|
|
511
|
+
if (value === undefined || value === null)
|
|
512
|
+
break;
|
|
513
|
+
value = value[parts[i]];
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (value === undefined || value === null) {
|
|
517
|
+
return match; // Keep original if not found
|
|
518
|
+
}
|
|
519
|
+
// Return stringified value
|
|
520
|
+
if (typeof value === 'object') {
|
|
521
|
+
return JSON.stringify(value);
|
|
522
|
+
}
|
|
523
|
+
return String(value);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
function parseHeaders(headersStr, context) {
|
|
527
|
+
if (!headersStr || !headersStr.trim())
|
|
528
|
+
return {};
|
|
529
|
+
const resolved = resolveVariables(headersStr, context);
|
|
530
|
+
// Try JSON format first: {"Authorization": "Bearer token"}
|
|
531
|
+
try {
|
|
532
|
+
const parsed = JSON.parse(resolved);
|
|
533
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
534
|
+
return parsed;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// Not JSON, try key:value format
|
|
539
|
+
}
|
|
540
|
+
// Try key:value format (one per line or semicolon-separated)
|
|
541
|
+
// Example: "Authorization: Bearer token; Content-Type: application/json"
|
|
542
|
+
const headers = {};
|
|
543
|
+
const pairs = resolved.split(/[;\n]/).map(s => s.trim()).filter(s => s);
|
|
544
|
+
for (const pair of pairs) {
|
|
545
|
+
const colonIndex = pair.indexOf(':');
|
|
546
|
+
if (colonIndex > 0) {
|
|
547
|
+
const key = pair.slice(0, colonIndex).trim();
|
|
548
|
+
const value = pair.slice(colonIndex + 1).trim();
|
|
549
|
+
headers[key] = value;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return headers;
|
|
553
|
+
}
|
|
554
|
+
async function parseResponse(response) {
|
|
555
|
+
const headers = {};
|
|
556
|
+
response.headers.forEach((value, key) => {
|
|
557
|
+
headers[key] = value;
|
|
558
|
+
});
|
|
559
|
+
let body;
|
|
560
|
+
const contentType = response.headers.get('content-type') || '';
|
|
561
|
+
if (contentType.includes('application/json')) {
|
|
562
|
+
body = await response.json();
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
body = await response.text();
|
|
566
|
+
}
|
|
567
|
+
return { status: response.status, headers, body };
|
|
568
|
+
}
|
|
569
|
+
function getValueByPath(obj, path) {
|
|
570
|
+
if (!path)
|
|
571
|
+
return obj;
|
|
572
|
+
const parts = path.split('.');
|
|
573
|
+
let current = obj;
|
|
574
|
+
for (const part of parts) {
|
|
575
|
+
if (current === null || current === undefined)
|
|
576
|
+
return undefined;
|
|
577
|
+
// Handle array indexing like "items[0]"
|
|
578
|
+
const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
|
|
579
|
+
if (arrayMatch) {
|
|
580
|
+
const [, key, index] = arrayMatch;
|
|
581
|
+
current = current[key];
|
|
582
|
+
if (Array.isArray(current)) {
|
|
583
|
+
current = current[parseInt(index, 10)];
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
current = current[part];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return current;
|
|
591
|
+
}
|
|
592
|
+
// Helper to extract value from XPath node
|
|
593
|
+
function getNodeValue(node) {
|
|
594
|
+
if (!node)
|
|
595
|
+
return null;
|
|
596
|
+
const n = node;
|
|
597
|
+
// Text node or attribute
|
|
598
|
+
if (n.nodeValue !== undefined) {
|
|
599
|
+
return n.nodeValue;
|
|
600
|
+
}
|
|
601
|
+
// Element node - get text content
|
|
602
|
+
if (n.textContent !== undefined) {
|
|
603
|
+
return n.textContent;
|
|
604
|
+
}
|
|
605
|
+
// Fallback to string representation
|
|
606
|
+
if (n.toString) {
|
|
607
|
+
return n.toString();
|
|
608
|
+
}
|
|
609
|
+
return null;
|
|
610
|
+
}
|