|
@@ -15,7 +15,7 @@
|
|
|
- Nodes **throw** on failure; they never return `success: false`.
|
|
- Nodes **throw** on failure; they never return `success: false`.
|
|
|
- Node config values arrive **pre-evaluated** - `WorkflowEngine::evaluateExpressions` walks the config before `execute()`, and a config string that is exactly one `{{...}}` keeps its native type. Nodes must not re-interpolate.
|
|
- Node config values arrive **pre-evaluated** - `WorkflowEngine::evaluateExpressions` walks the config before `execute()`, and a config string that is exactly one `{{...}}` keeps its native type. Nodes must not re-interpolate.
|
|
|
- All eleven nodes live in `nodes/core/`.
|
|
- All eleven nodes live in `nodes/core/`.
|
|
|
-- No imports between node files - each node is stored as one blob, so shared helpers are copied per file.
|
|
|
|
|
|
|
+- No imports between node files - each node is stored as one blob. Anything several nodes need lives on the `smartbotic` global, provided by the C++ script engine, not copied per file.
|
|
|
- Agreed field-path semantics for every new node: dot paths, `[n]` and bare numeric indices, `.length` on arrays, `undefined` for anything missing.
|
|
- Agreed field-path semantics for every new node: dot paths, `[n]` and bare numeric indices, `.length` on arrays, `undefined` for anything missing.
|
|
|
- QuickJS here is Bellard's build with **no Intl** - no named timezones.
|
|
- QuickJS here is Bellard's build with **no Intl** - no named timezones.
|
|
|
- Never write an em-dash or en-dash in any file, commit message or comment.
|
|
- Never write an em-dash or en-dash in any file, commit message or comment.
|
|
@@ -514,7 +514,192 @@ git commit -m "feat: the runner understands a node whose ports come from its con
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 5: Render derived ports and named inputs
|
|
|
|
|
|
|
+### Task 5: One path helper for every node
|
|
|
|
|
+
|
|
|
|
|
+Seven of the eleven nodes need to read a value out of nested data by dotted path. `if-condition.js` and `loop.js` each already carry their own copy with **different** semantics - `loop.js` silently skips a leading `data.` segment, `if-condition.js` does not - which is the sort of divergence that makes a workflow behave differently depending on which node reads the path. Rather than adding seven more copies, the helper goes on the `smartbotic` global once, beside the `utils.pick` and `utils.omit` that are already there.
|
|
|
|
|
+
|
|
|
|
|
+The two existing nodes are deliberately left alone: changing how `if-condition` or `loop` resolves a path would alter workflows people are already running.
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `src/runner/engine/script_engine.cpp:1550` (after the `utils.pick` registration, before `utils.omit`)
|
|
|
|
|
+- Create: `tests/nodes/utils-get-field-value.json`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+- Produces: `smartbotic.utils.getFieldValue(data, path)`, consumed by Tasks 9, 10, 12, 13, 14 and 15. Returns `undefined` for anything missing. Supports dotted paths, `key[n]` and bare numeric segments for arrays, and `.length` on an array. An empty or missing path returns `data` unchanged. No leading-`data.` special case - the path is taken literally.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: Register the helper**
|
|
|
|
|
+
|
|
|
|
|
+The surrounding registrations are C lambdas passed to `JS_NewCFunction`, each freeing every `JSValue` and `CString` it creates. Match that exactly - QuickJS is refcounted and a missed `JS_FreeValue` is a leak in a long-running runner.
|
|
|
|
|
+
|
|
|
|
|
+Insert after the closing `}, "pick", 2));` at line 1550, before the `// utils.omit` comment:
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+ // utils.getFieldValue(data, path) - Read a nested value by dotted path.
|
|
|
|
|
+ // Supports a.b.c, arrays via key[0] or a bare numeric segment, and .length
|
|
|
|
|
+ // on an array. Returns undefined for anything missing.
|
|
|
|
|
+ JS_SetPropertyStr(ctx, utils, "getFieldValue", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
|
|
+ if (argc < 1) {
|
|
|
|
|
+ return JS_ThrowTypeError(ctx, "utils.getFieldValue requires a value and a path");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (argc < 2 || JS_IsUndefined(argv[1]) || JS_IsNull(argv[1])) {
|
|
|
|
|
+ return JS_DupValue(ctx, argv[0]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const char* path_c = JS_ToCString(ctx, argv[1]);
|
|
|
|
|
+ if (!path_c) {
|
|
|
|
|
+ return JS_EXCEPTION;
|
|
|
|
|
+ }
|
|
|
|
|
+ std::string path(path_c);
|
|
|
|
|
+ JS_FreeCString(ctx, path_c);
|
|
|
|
|
+
|
|
|
|
|
+ if (path.empty()) {
|
|
|
|
|
+ return JS_DupValue(ctx, argv[0]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::vector<std::string> keys;
|
|
|
|
|
+ size_t start = 0;
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ size_t dot = path.find('.', start);
|
|
|
|
|
+ if (dot == std::string::npos) {
|
|
|
|
|
+ keys.push_back(path.substr(start));
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ keys.push_back(path.substr(start, dot - start));
|
|
|
|
|
+ start = dot + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JSValue current = JS_DupValue(ctx, argv[0]);
|
|
|
|
|
+
|
|
|
|
|
+ for (const auto& raw_key : keys) {
|
|
|
|
|
+ if (JS_IsUndefined(current) || JS_IsNull(current)) {
|
|
|
|
|
+ JS_FreeValue(ctx, current);
|
|
|
|
|
+ return JS_UNDEFINED;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ bool is_array = JS_IsArray(ctx, current);
|
|
|
|
|
+
|
|
|
|
|
+ if (raw_key == "length" && is_array) {
|
|
|
|
|
+ JSValue length = JS_GetPropertyStr(ctx, current, "length");
|
|
|
|
|
+ JS_FreeValue(ctx, current);
|
|
|
|
|
+ current = length;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // key[3] - step into the property, then index it
|
|
|
|
|
+ std::string key = raw_key;
|
|
|
|
|
+ bool has_index = false;
|
|
|
|
|
+ int64_t index = 0;
|
|
|
|
|
+ size_t bracket = raw_key.find('[');
|
|
|
|
|
+ if (bracket != std::string::npos && raw_key.back() == ']') {
|
|
|
|
|
+ std::string index_text = raw_key.substr(bracket + 1, raw_key.size() - bracket - 2);
|
|
|
|
|
+ if (!index_text.empty() &&
|
|
|
|
|
+ index_text.find_first_not_of("0123456789") == std::string::npos) {
|
|
|
|
|
+ key = raw_key.substr(0, bracket);
|
|
|
|
|
+ index = std::stoll(index_text);
|
|
|
|
|
+ has_index = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!key.empty()) {
|
|
|
|
|
+ bool numeric = key.find_first_not_of("0123456789") == std::string::npos;
|
|
|
|
|
+ if (numeric && is_array) {
|
|
|
|
|
+ JSValue element = JS_GetPropertyUint32(ctx, current, (uint32_t)std::stoll(key));
|
|
|
|
|
+ JS_FreeValue(ctx, current);
|
|
|
|
|
+ current = element;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (!JS_IsObject(current)) {
|
|
|
|
|
+ JS_FreeValue(ctx, current);
|
|
|
|
|
+ return JS_UNDEFINED;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSValue next = JS_GetPropertyStr(ctx, current, key.c_str());
|
|
|
|
|
+ JS_FreeValue(ctx, current);
|
|
|
|
|
+ current = next;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (has_index) {
|
|
|
|
|
+ if (!JS_IsArray(ctx, current)) {
|
|
|
|
|
+ JS_FreeValue(ctx, current);
|
|
|
|
|
+ return JS_UNDEFINED;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSValue element = JS_GetPropertyUint32(ctx, current, (uint32_t)index);
|
|
|
|
|
+ JS_FreeValue(ctx, current);
|
|
|
|
|
+ current = element;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return current;
|
|
|
|
|
+ }, "getFieldValue", 2));
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+If `<vector>` or `<string>` is not already included in this file, add it - check the existing includes first, since `utils.omit` already uses `std::vector<std::string>`, so both are almost certainly present.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Build and restart**
|
|
|
|
|
+
|
|
|
|
|
+Run:
|
|
|
|
|
+```bash
|
|
|
|
|
+cmake --build build -j$(nproc)
|
|
|
|
|
+pkill -f smartbotic-webserver; pkill -f smartbotic-runner; sleep 1
|
|
|
|
|
+(./build/smartbotic-webserver >/tmp/webserver.log 2>&1 &)
|
|
|
|
|
+(./build/smartbotic-runner >/tmp/runner.log 2>&1 &)
|
|
|
|
|
+sleep 3
|
|
|
|
|
+ss -ltnp | grep -E '8090|9011|9012'
|
|
|
|
|
+```
|
|
|
|
|
+Expected: compiles clean, all three ports listening again.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Write the verification case**
|
|
|
|
|
+
|
|
|
|
|
+A single `code` node exercising every path form, so a regression in the helper shows up as a concrete wrong value rather than a vague node failure.
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "name": "verify-utils-get-field-value",
|
|
|
|
|
+ "nodes": [
|
|
|
|
|
+ {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
|
|
|
|
|
+ {"id": "n2", "name": "Probe", "type": "code", "position": {"x": 0, "y": 100},
|
|
|
|
|
+ "config": {"code": "const d = { a: { b: { c: 7 } }, list: [{ id: 'x' }, { id: 'y' }], flat: [10, 20, 30] };\nconst g = smartbotic.utils.getFieldValue;\nreturn {\n nested: g(d, 'a.b.c'),\n bracket: g(d, 'list[1].id'),\n numeric: g(d, 'flat.2'),\n length: g(d, 'flat.length'),\n missing: g(d, 'a.nope.c') === undefined,\n throughNull: g({ a: null }, 'a.b') === undefined,\n emptyPath: g(d, '').a.b.c,\n notAnObject: g(d, 'a.b.c.d') === undefined\n};"}}
|
|
|
|
|
+ ],
|
|
|
|
|
+ "connections": [
|
|
|
|
|
+ {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "n2", "targetInput": "data"}
|
|
|
|
|
+ ],
|
|
|
|
|
+ "expect": {
|
|
|
|
|
+ "n2": {"status": "completed", "output": {"result": {
|
|
|
|
|
+ "nested": 7,
|
|
|
|
|
+ "bracket": "y",
|
|
|
|
|
+ "numeric": 30,
|
|
|
|
|
+ "length": 3,
|
|
|
|
|
+ "missing": true,
|
|
|
|
|
+ "throughNull": true,
|
|
|
|
|
+ "emptyPath": 7,
|
|
|
|
|
+ "notAnObject": true
|
|
|
|
|
+ }}}
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Run it**
|
|
|
|
|
+
|
|
|
|
|
+Run: `python3 scripts/verify-node.py tests/nodes/utils-get-field-value.json`
|
|
|
|
|
+Expected: `PASS`.
|
|
|
|
|
+
|
|
|
|
|
+A JSON `null` coming back where `undefined` was expected means the helper returned a QuickJS undefined that got serialized as null - check that the missing-path branches return `JS_UNDEFINED` rather than `JS_NULL`. The case compares against `undefined` inside the script precisely so this distinction is caught in JavaScript, before serialization.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: Re-run the baseline**
|
|
|
|
|
+
|
|
|
|
|
+Run: `python3 scripts/verify-node.py tests/nodes/baseline-if-condition.json`
|
|
|
|
|
+Expected: `PASS`. `if-condition` keeps its own copy of the helper and must be untouched by this task.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add src/runner/engine/script_engine.cpp tests/nodes/utils-get-field-value.json
|
|
|
|
|
+git commit -m "feat: one path helper on the smartbotic global, for every node to share"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 6: Render derived ports and named inputs
|
|
|
|
|
|
|
|
Ports are derived at **render time** from `data.dynamicOutputs` and `data.config`, not precomputed. `data.config` already updates when a node's config is saved, so the ports follow a rule being added or removed with no extra wiring.
|
|
Ports are derived at **render time** from `data.dynamicOutputs` and `data.config`, not precomputed. `data.config` already updates when a node's config is saved, so the ports follow a rule being added or removed with no extra wiring.
|
|
|
|
|
|
|
@@ -524,7 +709,7 @@ Ports are derived at **render time** from `data.dynamicOutputs` and `data.config
|
|
|
|
|
|
|
|
**Interfaces:**
|
|
**Interfaces:**
|
|
|
- Consumes: `dynamicOutputs` on the REST node payload from Task 3.
|
|
- Consumes: `dynamicOutputs` on the REST node payload from Task 3.
|
|
|
-- Produces: `resolveOutputs(staticOutputs, dynamicOutputs, config): NodeOutput[]`, exported from `WorkflowNode.tsx` for reuse in Task 6.
|
|
|
|
|
|
|
+- Produces: `resolveOutputs(staticOutputs, dynamicOutputs, config): NodeOutput[]`, exported from `WorkflowNode.tsx` for reuse in Task 7.
|
|
|
|
|
|
|
|
- [ ] **Step 1: Extend the types**
|
|
- [ ] **Step 1: Extend the types**
|
|
|
|
|
|
|
@@ -656,7 +841,7 @@ Add the `inputs` binding beside `outputs` in the component body:
|
|
|
- [ ] **Step 4: Check it compiles**
|
|
- [ ] **Step 4: Check it compiles**
|
|
|
|
|
|
|
|
Run: `cd webui && npm run lint && npm run build`
|
|
Run: `cd webui && npm run lint && npm run build`
|
|
|
-Expected: no TypeScript errors. `resolveOutputs` is exported but not yet used elsewhere - that is fine, it is consumed in Task 6.
|
|
|
|
|
|
|
+Expected: no TypeScript errors. `resolveOutputs` is exported but not yet used elsewhere - that is fine, it is consumed in Task 7.
|
|
|
|
|
|
|
|
- [ ] **Step 5: Commit**
|
|
- [ ] **Step 5: Commit**
|
|
|
|
|
|
|
@@ -667,7 +852,7 @@ git commit -m "feat(webui): draw ports a node derives from its config, and named
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 6: Wire derived ports through the editor
|
|
|
|
|
|
|
+### Task 7: Wire derived ports through the editor
|
|
|
|
|
|
|
|
Three sites build a node's `data`. Each needs to pass `dynamicOutputs` and `inputs` through rather than only the static `outputs`. Saving a config that removes a rule must also drop the edges that pointed at the vanished port.
|
|
Three sites build a node's `data`. Each needs to pass `dynamicOutputs` and `inputs` through rather than only the static `outputs`. Saving a config that removes a rule must also drop the edges that pointed at the vanished port.
|
|
|
|
|
|
|
@@ -675,7 +860,7 @@ Three sites build a node's `data`. Each needs to pass `dynamicOutputs` and `inpu
|
|
|
- Modify: `webui/src/pages/WorkflowEditorPage.tsx:894-918` (view mode), `:968-992` (load), `:1307-1331` (addNode), `:1628-1641` (saveNodeConfig)
|
|
- Modify: `webui/src/pages/WorkflowEditorPage.tsx:894-918` (view mode), `:968-992` (load), `:1307-1331` (addNode), `:1628-1641` (saveNodeConfig)
|
|
|
|
|
|
|
|
**Interfaces:**
|
|
**Interfaces:**
|
|
|
-- Consumes: `resolveOutputs` from Task 5.
|
|
|
|
|
|
|
+- Consumes: `resolveOutputs` from Task 6.
|
|
|
|
|
|
|
|
- [ ] **Step 1: Pass the definition through at all three construction sites**
|
|
- [ ] **Step 1: Pass the definition through at all three construction sites**
|
|
|
|
|
|
|
@@ -749,7 +934,7 @@ git commit -m "feat(webui): derived ports follow a node's config, and stale edge
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 7: Set / Edit Fields
|
|
|
|
|
|
|
+### Task 8: Set / Edit Fields
|
|
|
|
|
|
|
|
The node the roadmap calls the single biggest win. Because the engine pre-evaluates config, this node's whole job is placing already-computed values at the right paths.
|
|
The node the roadmap calls the single biggest win. Because the engine pre-evaluates config, this node's whole job is placing already-computed values at the right paths.
|
|
|
|
|
|
|
@@ -944,17 +1129,17 @@ git commit -m "feat: a Set Fields node, so shaping data stops needing a Code nod
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 8: Switch
|
|
|
|
|
|
|
+### Task 9: Switch
|
|
|
|
|
|
|
|
-The first node with derived ports. Its passing test is the proof that Tasks 2 through 6 work end to end.
|
|
|
|
|
|
|
+The first node with derived ports. Its passing test is the proof that Tasks 2 through 7 work end to end.
|
|
|
|
|
|
|
|
**Files:**
|
|
**Files:**
|
|
|
- Create: `nodes/core/switch.js`
|
|
- Create: `nodes/core/switch.js`
|
|
|
- Create: `tests/nodes/switch.json`
|
|
- Create: `tests/nodes/switch.json`
|
|
|
|
|
|
|
|
**Interfaces:**
|
|
**Interfaces:**
|
|
|
-- Consumes: the `dynamicOutputs` parsing and rendering from Tasks 2-6.
|
|
|
|
|
-- Produces: `getFieldValue(data, path)` - the agreed path helper, copied verbatim into Tasks 9, 11, 12.
|
|
|
|
|
|
|
+- Consumes: the `dynamicOutputs` parsing and rendering from Tasks 2-4 and 6-7.
|
|
|
|
|
+- Consumes: `smartbotic.utils.getFieldValue(data, path)` from Task 5.
|
|
|
|
|
|
|
|
- [ ] **Step 1: Write the node**
|
|
- [ ] **Step 1: Write the node**
|
|
|
|
|
|
|
@@ -1031,49 +1216,6 @@ const outputSchema = {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-function getFieldValue(data, path) {
|
|
|
|
|
- if (!path) return data;
|
|
|
|
|
-
|
|
|
|
|
- const keys = String(path).split('.');
|
|
|
|
|
- let value = data;
|
|
|
|
|
-
|
|
|
|
|
- for (const key of keys) {
|
|
|
|
|
- if (value === null || value === undefined) return undefined;
|
|
|
|
|
-
|
|
|
|
|
- if (key === 'length' && Array.isArray(value)) {
|
|
|
|
|
- value = value.length;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
|
|
|
|
- if (arrayMatch) {
|
|
|
|
|
- const objKey = arrayMatch[1];
|
|
|
|
|
- const index = arrayMatch[2];
|
|
|
|
|
- if (objKey && typeof value === 'object' && objKey in value) {
|
|
|
|
|
- value = value[objKey];
|
|
|
|
|
- }
|
|
|
|
|
- if (Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(index, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (/^\d+$/.test(key) && Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(key, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (typeof value === 'object' && key in value) {
|
|
|
|
|
- value = value[key];
|
|
|
|
|
- } else {
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return value;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function ruleMatches(fieldValue, rule) {
|
|
function ruleMatches(fieldValue, rule) {
|
|
|
const compare = rule.value;
|
|
const compare = rule.value;
|
|
|
|
|
|
|
@@ -1116,7 +1258,7 @@ function ruleMatches(fieldValue, rule) {
|
|
|
async function execute(config, input, context) {
|
|
async function execute(config, input, context) {
|
|
|
const rules = Array.isArray(config.rules) ? config.rules : [];
|
|
const rules = Array.isArray(config.rules) ? config.rules : [];
|
|
|
const data = input;
|
|
const data = input;
|
|
|
- const fieldValue = getFieldValue(data, config.field);
|
|
|
|
|
|
|
+ const fieldValue = smartbotic.utils.getFieldValue(data, config.field);
|
|
|
|
|
|
|
|
for (let i = 0; i < rules.length; i++) {
|
|
for (let i = 0; i < rules.length; i++) {
|
|
|
if (ruleMatches(fieldValue, rules[i] || {})) {
|
|
if (ruleMatches(fieldValue, rules[i] || {})) {
|
|
@@ -1212,7 +1354,7 @@ git commit -m "feat: a Switch node that grows an output per rule"
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 9: Filter
|
|
|
|
|
|
|
+### Task 10: Filter
|
|
|
|
|
|
|
|
Both ports carry data at once. That works because this node emits **no** `_activeBranch` - the engine then reads `output[sourceOutput]` for each named connection independently.
|
|
Both ports carry data at once. That works because this node emits **no** `_activeBranch` - the engine then reads `output[sourceOutput]` for each named connection independently.
|
|
|
|
|
|
|
@@ -1221,7 +1363,7 @@ Both ports carry data at once. That works because this node emits **no** `_activ
|
|
|
- Create: `tests/nodes/filter.json`
|
|
- Create: `tests/nodes/filter.json`
|
|
|
|
|
|
|
|
**Interfaces:**
|
|
**Interfaces:**
|
|
|
-- Consumes: `getFieldValue` from Task 8 (copied, not imported).
|
|
|
|
|
|
|
+- Consumes: `smartbotic.utils.getFieldValue(data, path)` from Task 5.
|
|
|
|
|
|
|
|
- [ ] **Step 1: Write the node**
|
|
- [ ] **Step 1: Write the node**
|
|
|
|
|
|
|
@@ -1297,56 +1439,13 @@ const outputSchema = {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-function getFieldValue(data, path) {
|
|
|
|
|
- if (!path) return data;
|
|
|
|
|
-
|
|
|
|
|
- const keys = String(path).split('.');
|
|
|
|
|
- let value = data;
|
|
|
|
|
-
|
|
|
|
|
- for (const key of keys) {
|
|
|
|
|
- if (value === null || value === undefined) return undefined;
|
|
|
|
|
-
|
|
|
|
|
- if (key === 'length' && Array.isArray(value)) {
|
|
|
|
|
- value = value.length;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
|
|
|
|
- if (arrayMatch) {
|
|
|
|
|
- const objKey = arrayMatch[1];
|
|
|
|
|
- const index = arrayMatch[2];
|
|
|
|
|
- if (objKey && typeof value === 'object' && objKey in value) {
|
|
|
|
|
- value = value[objKey];
|
|
|
|
|
- }
|
|
|
|
|
- if (Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(index, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (/^\d+$/.test(key) && Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(key, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (typeof value === 'object' && key in value) {
|
|
|
|
|
- value = value[key];
|
|
|
|
|
- } else {
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return value;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function isEmpty(value) {
|
|
function isEmpty(value) {
|
|
|
return value === undefined || value === null || value === '' ||
|
|
return value === undefined || value === null || value === '' ||
|
|
|
(Array.isArray(value) && value.length === 0);
|
|
(Array.isArray(value) && value.length === 0);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function conditionHolds(item, condition) {
|
|
function conditionHolds(item, condition) {
|
|
|
- const fieldValue = getFieldValue(item, condition.field);
|
|
|
|
|
|
|
+ const fieldValue = smartbotic.utils.getFieldValue(item, condition.field);
|
|
|
const compare = condition.value;
|
|
const compare = condition.value;
|
|
|
|
|
|
|
|
switch (condition.operator) {
|
|
switch (condition.operator) {
|
|
@@ -1394,7 +1493,7 @@ async function execute(config, input, context) {
|
|
|
if (Array.isArray(inputField)) {
|
|
if (Array.isArray(inputField)) {
|
|
|
items = inputField;
|
|
items = inputField;
|
|
|
} else {
|
|
} else {
|
|
|
- items = getFieldValue(input, inputField || 'data');
|
|
|
|
|
|
|
+ items = smartbotic.utils.getFieldValue(input, inputField || 'data');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!Array.isArray(items)) {
|
|
if (!Array.isArray(items)) {
|
|
@@ -1484,16 +1583,16 @@ git commit -m "feat: a Filter node, with the dropped items kept on their own out
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 10: Merge
|
|
|
|
|
|
|
+### Task 11: Merge
|
|
|
|
|
|
|
|
-The first node with two input handles. Its passing test proves the `const inputs` work from Tasks 2 and 5.
|
|
|
|
|
|
|
+The first node with two input handles. Its passing test proves the `const inputs` work from Tasks 2 and 6.
|
|
|
|
|
|
|
|
**Files:**
|
|
**Files:**
|
|
|
- Create: `nodes/core/merge.js`
|
|
- Create: `nodes/core/merge.js`
|
|
|
- Create: `tests/nodes/merge.json`
|
|
- Create: `tests/nodes/merge.json`
|
|
|
|
|
|
|
|
**Interfaces:**
|
|
**Interfaces:**
|
|
|
-- Consumes: `const inputs` parsing from Task 2, handle rendering from Task 5.
|
|
|
|
|
|
|
+- Consumes: `const inputs` parsing from Task 2, handle rendering from Task 6.
|
|
|
|
|
|
|
|
- [ ] **Step 1: Write the node**
|
|
- [ ] **Step 1: Write the node**
|
|
|
|
|
|
|
@@ -1659,7 +1758,7 @@ git commit -m "feat: a Merge node, and two input handles for it to use"
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 11: Split Out and Aggregate
|
|
|
|
|
|
|
+### Task 12: Split Out and Aggregate
|
|
|
|
|
|
|
|
A pair, because each is the other's inverse and they are tested against each other.
|
|
A pair, because each is the other's inverse and they are tested against each other.
|
|
|
|
|
|
|
@@ -1668,7 +1767,7 @@ A pair, because each is the other's inverse and they are tested against each oth
|
|
|
- Create: `tests/nodes/split-aggregate.json`
|
|
- Create: `tests/nodes/split-aggregate.json`
|
|
|
|
|
|
|
|
**Interfaces:**
|
|
**Interfaces:**
|
|
|
-- Consumes: `getFieldValue` from Task 8 (copied into both files).
|
|
|
|
|
|
|
+- Consumes: `smartbotic.utils.getFieldValue(data, path)` from Task 5.
|
|
|
|
|
|
|
|
- [ ] **Step 1: Write split-out**
|
|
- [ ] **Step 1: Write split-out**
|
|
|
|
|
|
|
@@ -1727,49 +1826,6 @@ const outputSchema = {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-function getFieldValue(data, path) {
|
|
|
|
|
- if (!path) return data;
|
|
|
|
|
-
|
|
|
|
|
- const keys = String(path).split('.');
|
|
|
|
|
- let value = data;
|
|
|
|
|
-
|
|
|
|
|
- for (const key of keys) {
|
|
|
|
|
- if (value === null || value === undefined) return undefined;
|
|
|
|
|
-
|
|
|
|
|
- if (key === 'length' && Array.isArray(value)) {
|
|
|
|
|
- value = value.length;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
|
|
|
|
- if (arrayMatch) {
|
|
|
|
|
- const objKey = arrayMatch[1];
|
|
|
|
|
- const index = arrayMatch[2];
|
|
|
|
|
- if (objKey && typeof value === 'object' && objKey in value) {
|
|
|
|
|
- value = value[objKey];
|
|
|
|
|
- }
|
|
|
|
|
- if (Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(index, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (/^\d+$/.test(key) && Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(key, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (typeof value === 'object' && key in value) {
|
|
|
|
|
- value = value[key];
|
|
|
|
|
- } else {
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return value;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function setByPath(target, path, value) {
|
|
function setByPath(target, path, value) {
|
|
|
const keys = String(path).split('.');
|
|
const keys = String(path).split('.');
|
|
|
let cursor = target;
|
|
let cursor = target;
|
|
@@ -1795,7 +1851,7 @@ async function execute(config, input, context) {
|
|
|
if (Array.isArray(field)) {
|
|
if (Array.isArray(field)) {
|
|
|
list = field;
|
|
list = field;
|
|
|
} else {
|
|
} else {
|
|
|
- list = getFieldValue(input, field);
|
|
|
|
|
|
|
+ list = smartbotic.utils.getFieldValue(input, field);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!Array.isArray(list)) {
|
|
if (!Array.isArray(list)) {
|
|
@@ -1822,7 +1878,7 @@ async function execute(config, input, context) {
|
|
|
} else if (include === 'selected') {
|
|
} else if (include === 'selected') {
|
|
|
for (const path of includeFields) {
|
|
for (const path of includeFields) {
|
|
|
if (path) {
|
|
if (path) {
|
|
|
- setByPath(item, path, getFieldValue(parent, path));
|
|
|
|
|
|
|
+ setByPath(item, path, smartbotic.utils.getFieldValue(parent, path));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1894,49 +1950,6 @@ const outputSchema = {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-function getFieldValue(data, path) {
|
|
|
|
|
- if (!path) return data;
|
|
|
|
|
-
|
|
|
|
|
- const keys = String(path).split('.');
|
|
|
|
|
- let value = data;
|
|
|
|
|
-
|
|
|
|
|
- for (const key of keys) {
|
|
|
|
|
- if (value === null || value === undefined) return undefined;
|
|
|
|
|
-
|
|
|
|
|
- if (key === 'length' && Array.isArray(value)) {
|
|
|
|
|
- value = value.length;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
|
|
|
|
- if (arrayMatch) {
|
|
|
|
|
- const objKey = arrayMatch[1];
|
|
|
|
|
- const index = arrayMatch[2];
|
|
|
|
|
- if (objKey && typeof value === 'object' && objKey in value) {
|
|
|
|
|
- value = value[objKey];
|
|
|
|
|
- }
|
|
|
|
|
- if (Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(index, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (/^\d+$/.test(key) && Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(key, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (typeof value === 'object' && key in value) {
|
|
|
|
|
- value = value[key];
|
|
|
|
|
- } else {
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return value;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
async function execute(config, input, context) {
|
|
async function execute(config, input, context) {
|
|
|
const inputField = config.inputField;
|
|
const inputField = config.inputField;
|
|
|
const outputField = config.outputField || 'items';
|
|
const outputField = config.outputField || 'items';
|
|
@@ -1947,7 +1960,7 @@ async function execute(config, input, context) {
|
|
|
if (Array.isArray(inputField)) {
|
|
if (Array.isArray(inputField)) {
|
|
|
items = inputField;
|
|
items = inputField;
|
|
|
} else {
|
|
} else {
|
|
|
- items = getFieldValue(input, inputField || 'data');
|
|
|
|
|
|
|
+ items = smartbotic.utils.getFieldValue(input, inputField || 'data');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!Array.isArray(items)) {
|
|
if (!Array.isArray(items)) {
|
|
@@ -1955,7 +1968,7 @@ async function execute(config, input, context) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const values = fieldToAggregate
|
|
const values = fieldToAggregate
|
|
|
- ? items.map(function (item) { return getFieldValue(item, fieldToAggregate); })
|
|
|
|
|
|
|
+ ? items.map(function (item) { return smartbotic.utils.getFieldValue(item, fieldToAggregate); })
|
|
|
: items;
|
|
: items;
|
|
|
|
|
|
|
|
if (!groupBy) {
|
|
if (!groupBy) {
|
|
@@ -1968,7 +1981,7 @@ async function execute(config, input, context) {
|
|
|
const groups = {};
|
|
const groups = {};
|
|
|
const order = [];
|
|
const order = [];
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
- const key = String(getFieldValue(items[i], groupBy));
|
|
|
|
|
|
|
+ const key = String(smartbotic.utils.getFieldValue(items[i], groupBy));
|
|
|
if (groups[key] === undefined) {
|
|
if (groups[key] === undefined) {
|
|
|
groups[key] = [];
|
|
groups[key] = [];
|
|
|
order.push(key);
|
|
order.push(key);
|
|
@@ -2039,7 +2052,7 @@ git commit -m "feat: Split Out and Aggregate, so Loop is not the only way to res
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 12: Sort, Limit and Dedupe
|
|
|
|
|
|
|
+### Task 13: Sort, Limit and Dedupe
|
|
|
|
|
|
|
|
One node. The three are always used together and each alone is ten lines.
|
|
One node. The three are always used together and each alone is ten lines.
|
|
|
|
|
|
|
@@ -2048,7 +2061,7 @@ One node. The three are always used together and each alone is ten lines.
|
|
|
- Create: `tests/nodes/sort-limit-dedupe.json`
|
|
- Create: `tests/nodes/sort-limit-dedupe.json`
|
|
|
|
|
|
|
|
**Interfaces:**
|
|
**Interfaces:**
|
|
|
-- Consumes: `getFieldValue` from Task 8 (copied).
|
|
|
|
|
|
|
+- Consumes: `smartbotic.utils.getFieldValue(data, path)` from Task 5.
|
|
|
|
|
|
|
|
- [ ] **Step 1: Write the node**
|
|
- [ ] **Step 1: Write the node**
|
|
|
|
|
|
|
@@ -2131,49 +2144,6 @@ const outputSchema = {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-function getFieldValue(data, path) {
|
|
|
|
|
- if (!path) return data;
|
|
|
|
|
-
|
|
|
|
|
- const keys = String(path).split('.');
|
|
|
|
|
- let value = data;
|
|
|
|
|
-
|
|
|
|
|
- for (const key of keys) {
|
|
|
|
|
- if (value === null || value === undefined) return undefined;
|
|
|
|
|
-
|
|
|
|
|
- if (key === 'length' && Array.isArray(value)) {
|
|
|
|
|
- value = value.length;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
|
|
|
|
- if (arrayMatch) {
|
|
|
|
|
- const objKey = arrayMatch[1];
|
|
|
|
|
- const index = arrayMatch[2];
|
|
|
|
|
- if (objKey && typeof value === 'object' && objKey in value) {
|
|
|
|
|
- value = value[objKey];
|
|
|
|
|
- }
|
|
|
|
|
- if (Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(index, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (/^\d+$/.test(key) && Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(key, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (typeof value === 'object' && key in value) {
|
|
|
|
|
- value = value[key];
|
|
|
|
|
- } else {
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return value;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function compareValues(left, right, compareAs) {
|
|
function compareValues(left, right, compareAs) {
|
|
|
if (compareAs === 'number' || (compareAs === 'auto' && typeof left === 'number' && typeof right === 'number')) {
|
|
if (compareAs === 'number' || (compareAs === 'auto' && typeof left === 'number' && typeof right === 'number')) {
|
|
|
const a = Number(left);
|
|
const a = Number(left);
|
|
@@ -2199,7 +2169,7 @@ async function execute(config, input, context) {
|
|
|
if (Array.isArray(inputField)) {
|
|
if (Array.isArray(inputField)) {
|
|
|
items = inputField;
|
|
items = inputField;
|
|
|
} else {
|
|
} else {
|
|
|
- items = getFieldValue(input, inputField || 'data');
|
|
|
|
|
|
|
+ items = smartbotic.utils.getFieldValue(input, inputField || 'data');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!Array.isArray(items)) {
|
|
if (!Array.isArray(items)) {
|
|
@@ -2213,7 +2183,7 @@ async function execute(config, input, context) {
|
|
|
const seen = {};
|
|
const seen = {};
|
|
|
const unique = [];
|
|
const unique = [];
|
|
|
for (const item of working) {
|
|
for (const item of working) {
|
|
|
- const key = String(getFieldValue(item, dedupeBy));
|
|
|
|
|
|
|
+ const key = String(smartbotic.utils.getFieldValue(item, dedupeBy));
|
|
|
if (seen[key] === true) {
|
|
if (seen[key] === true) {
|
|
|
removedDuplicates++;
|
|
removedDuplicates++;
|
|
|
continue;
|
|
continue;
|
|
@@ -2229,8 +2199,8 @@ async function execute(config, input, context) {
|
|
|
for (const rule of sortBy) {
|
|
for (const rule of sortBy) {
|
|
|
if (!rule || !rule.field) continue;
|
|
if (!rule || !rule.field) continue;
|
|
|
const order = compareValues(
|
|
const order = compareValues(
|
|
|
- getFieldValue(left, rule.field),
|
|
|
|
|
- getFieldValue(right, rule.field),
|
|
|
|
|
|
|
+ smartbotic.utils.getFieldValue(left, rule.field),
|
|
|
|
|
+ smartbotic.utils.getFieldValue(right, rule.field),
|
|
|
rule.type || 'auto'
|
|
rule.type || 'auto'
|
|
|
);
|
|
);
|
|
|
if (order !== 0) {
|
|
if (order !== 0) {
|
|
@@ -2306,14 +2276,14 @@ git commit -m "feat: sorting, top-N and dedupe in one node"
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 13: Template and JSON
|
|
|
|
|
|
|
+### Task 14: Template and JSON
|
|
|
|
|
|
|
|
**Files:**
|
|
**Files:**
|
|
|
- Create: `nodes/core/template.js`, `nodes/core/json.js`
|
|
- Create: `nodes/core/template.js`, `nodes/core/json.js`
|
|
|
- Create: `tests/nodes/template-json.json`
|
|
- Create: `tests/nodes/template-json.json`
|
|
|
|
|
|
|
|
**Interfaces:**
|
|
**Interfaces:**
|
|
|
-- Consumes: `getFieldValue` from Task 8 (copied into `json.js`).
|
|
|
|
|
|
|
+- Consumes: `smartbotic.utils.getFieldValue(data, path)` from Task 5.
|
|
|
|
|
|
|
|
- [ ] **Step 1: Write template**
|
|
- [ ] **Step 1: Write template**
|
|
|
|
|
|
|
@@ -2450,55 +2420,12 @@ const outputSchema = {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-function getFieldValue(data, path) {
|
|
|
|
|
- if (!path) return data;
|
|
|
|
|
-
|
|
|
|
|
- const keys = String(path).split('.');
|
|
|
|
|
- let value = data;
|
|
|
|
|
-
|
|
|
|
|
- for (const key of keys) {
|
|
|
|
|
- if (value === null || value === undefined) return undefined;
|
|
|
|
|
-
|
|
|
|
|
- if (key === 'length' && Array.isArray(value)) {
|
|
|
|
|
- value = value.length;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
|
|
|
|
- if (arrayMatch) {
|
|
|
|
|
- const objKey = arrayMatch[1];
|
|
|
|
|
- const index = arrayMatch[2];
|
|
|
|
|
- if (objKey && typeof value === 'object' && objKey in value) {
|
|
|
|
|
- value = value[objKey];
|
|
|
|
|
- }
|
|
|
|
|
- if (Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(index, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (/^\d+$/.test(key) && Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(key, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (typeof value === 'object' && key in value) {
|
|
|
|
|
- value = value[key];
|
|
|
|
|
- } else {
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return value;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
async function execute(config, input, context) {
|
|
async function execute(config, input, context) {
|
|
|
const operation = config.operation || 'parse';
|
|
const operation = config.operation || 'parse';
|
|
|
const outputField = config.outputField || 'value';
|
|
const outputField = config.outputField || 'value';
|
|
|
const onError = config.onError || 'throw';
|
|
const onError = config.onError || 'throw';
|
|
|
|
|
|
|
|
- const source = getFieldValue(input, config.inputField || 'data');
|
|
|
|
|
|
|
+ const source = smartbotic.utils.getFieldValue(input, config.inputField || 'data');
|
|
|
const result = {};
|
|
const result = {};
|
|
|
|
|
|
|
|
if (operation === 'stringify') {
|
|
if (operation === 'stringify') {
|
|
@@ -2509,7 +2436,7 @@ async function execute(config, input, context) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (operation === 'extract') {
|
|
if (operation === 'extract') {
|
|
|
- result[outputField] = getFieldValue(source, config.path);
|
|
|
|
|
|
|
+ result[outputField] = smartbotic.utils.getFieldValue(source, config.path);
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -2588,7 +2515,7 @@ git commit -m "feat: Template and JSON nodes"
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 14: Date and Time
|
|
|
|
|
|
|
+### Task 15: Date and Time
|
|
|
|
|
|
|
|
Offsets only. QuickJS here has no `Intl`, so `Europe/Budapest` cannot be resolved and the node says so in its description rather than pretending.
|
|
Offsets only. QuickJS here has no `Intl`, so `Europe/Budapest` cannot be resolved and the node says so in its description rather than pretending.
|
|
|
|
|
|
|
@@ -2684,49 +2611,6 @@ const UNIT_MS = {
|
|
|
weeks: 604800000
|
|
weeks: 604800000
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-function getFieldValue(data, path) {
|
|
|
|
|
- if (!path) return data;
|
|
|
|
|
-
|
|
|
|
|
- const keys = String(path).split('.');
|
|
|
|
|
- let value = data;
|
|
|
|
|
-
|
|
|
|
|
- for (const key of keys) {
|
|
|
|
|
- if (value === null || value === undefined) return undefined;
|
|
|
|
|
-
|
|
|
|
|
- if (key === 'length' && Array.isArray(value)) {
|
|
|
|
|
- value = value.length;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
|
|
|
|
- if (arrayMatch) {
|
|
|
|
|
- const objKey = arrayMatch[1];
|
|
|
|
|
- const index = arrayMatch[2];
|
|
|
|
|
- if (objKey && typeof value === 'object' && objKey in value) {
|
|
|
|
|
- value = value[objKey];
|
|
|
|
|
- }
|
|
|
|
|
- if (Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(index, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (/^\d+$/.test(key) && Array.isArray(value)) {
|
|
|
|
|
- value = value[parseInt(key, 10)];
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (typeof value === 'object' && key in value) {
|
|
|
|
|
- value = value[key];
|
|
|
|
|
- } else {
|
|
|
|
|
- return undefined;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return value;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function toTimestamp(value, label) {
|
|
function toTimestamp(value, label) {
|
|
|
if (value === undefined || value === null || value === '') {
|
|
if (value === undefined || value === null || value === '') {
|
|
|
throw new Error('Date and Time: no date found at "' + label + '"');
|
|
throw new Error('Date and Time: no date found at "' + label + '"');
|
|
@@ -2804,7 +2688,7 @@ async function execute(config, input, context) {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const raw = getFieldValue(input, config.inputField || 'data');
|
|
|
|
|
|
|
+ const raw = smartbotic.utils.getFieldValue(input, config.inputField || 'data');
|
|
|
const timestamp = toTimestamp(raw, config.inputField || 'data');
|
|
const timestamp = toTimestamp(raw, config.inputField || 'data');
|
|
|
|
|
|
|
|
if (operation === 'parse') {
|
|
if (operation === 'parse') {
|
|
@@ -2821,7 +2705,7 @@ async function execute(config, input, context) {
|
|
|
|
|
|
|
|
if (operation === 'diff') {
|
|
if (operation === 'diff') {
|
|
|
const other = toTimestamp(
|
|
const other = toTimestamp(
|
|
|
- getFieldValue(input, config.secondField),
|
|
|
|
|
|
|
+ smartbotic.utils.getFieldValue(input, config.secondField),
|
|
|
config.secondField || 'secondField'
|
|
config.secondField || 'secondField'
|
|
|
);
|
|
);
|
|
|
result[outputField] = (timestamp - other) / step;
|
|
result[outputField] = (timestamp - other) / step;
|
|
@@ -2889,7 +2773,7 @@ git commit -m "feat: a Date and Time node, honest about having no timezone datab
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Task 15: Stop and Error, and the docs
|
|
|
|
|
|
|
+### Task 16: Stop and Error, and the docs
|
|
|
|
|
|
|
|
**Files:**
|
|
**Files:**
|
|
|
- Create: `nodes/core/stop-and-error.js`
|
|
- Create: `nodes/core/stop-and-error.js`
|
|
@@ -3013,7 +2897,20 @@ well-stocked, those matter more than any ten integrations.
|
|
|
|
|
|
|
|
- [ ] **Step 5: Document the two new declarations**
|
|
- [ ] **Step 5: Document the two new declarations**
|
|
|
|
|
|
|
|
-In `docs/nodes.md`, after the "Custom Outputs" section (line 167), add:
|
|
|
|
|
|
|
+In `docs/nodes.md`, in the "Utilities" section, add the shared path helper to the listed `smartbotic.utils` calls:
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+// Read a nested value by dotted path
|
|
|
|
|
+const city = smartbotic.utils.getFieldValue(input, 'data.user.address.city');
|
|
|
|
|
+const second = smartbotic.utils.getFieldValue(input, 'data.items[1].id');
|
|
|
|
|
+const howMany = smartbotic.utils.getFieldValue(input, 'data.items.length');
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Missing paths return `undefined`. Arrays are reached with `key[0]` or a bare
|
|
|
|
|
+numeric segment, and `.length` works on an array. The path is taken literally,
|
|
|
|
|
+with no special handling of a leading `data.` segment.
|
|
|
|
|
+
|
|
|
|
|
+Then, after the "Custom Outputs" section (line 167), add:
|
|
|
|
|
|
|
|
````markdown
|
|
````markdown
|
|
|
## Outputs That Come From Config
|
|
## Outputs That Come From Config
|
|
@@ -3088,6 +2985,6 @@ git commit -m "feat: a Stop and Error node, and docs for the two new port declar
|
|
|
|
|
|
|
|
**If a branch test routes wrongly.** Print the node's full output from the harness listing. `_activeBranch` must name a port that also exists as a key on the returned object - `{_activeBranch: 'case1', case1: data}`. A missing key falls back to `output.data` (`workflow_engine.cpp:1243`), which looks like success but carries the wrong payload.
|
|
**If a branch test routes wrongly.** Print the node's full output from the harness listing. `_activeBranch` must name a port that also exists as a key on the returned object - `{_activeBranch: 'case1', case1: data}`. A missing key falls back to `output.data` (`workflow_engine.cpp:1243`), which looks like success but carries the wrong payload.
|
|
|
|
|
|
|
|
-**If the editor draws no ports for switch.** Check the REST payload first (`curl .../nodes/switch | jq .dynamicOutputs`). If it is there, the break is in `resolveOutputs` or in the three editor sites from Task 6, not in the C++.
|
|
|
|
|
|
|
+**If the editor draws no ports for switch.** Check the REST payload first (`curl .../nodes/switch | jq .dynamicOutputs`). If it is there, the break is in `resolveOutputs` or in the three editor sites from Task 7, not in the C++.
|
|
|
|
|
|
|
|
**Restarting services.** Always both, always after any C++ build. The runner caches node definitions over a gRPC stream, so a stale runner will happily execute the previous version of a node and the failure looks like a node bug.
|
|
**Restarting services.** Always both, always after any C++ build. The runner caches node definitions over a gRPC stream, so a stale runner will happily execute the previous version of a node and the failure looks like a node bug.
|