From 9d71773dc39f546caeefb983e4cc731c80024344 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 28 Feb 2026 18:02:52 +0530 Subject: [PATCH 1/2] fix: correct POSITIVE_CHECKS logic in seo-audit and 429 retry exhaustion in fetchWithRetry - seo-audit: both branches of the POSITIVE_CHECKS if/else were assigning `checkData.passed` identically, making the distinction a no-op. Normal checks now correctly invert with `!checkData.passed` so that a DataForSEO "passed=true" (problem absent) maps to isPassed=true. - fetchWithRetry: a 429 on the final retry attempt fell through the if-block and returned the rate-limit Response to the caller instead of throwing. Now throws FetchRetryError("Rate limit exceeded after N attempts") consistent with how 5xx exhaustion is handled. --- .gitignore | 3 +++ packages/nodes/src/integrations/dataforseo/seo-audit.ts | 5 ++++- packages/nodes/src/utils/http.ts | 8 ++++++++ packages/nodes/src/utils/index.ts | 5 +++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 21fd20c..35d5ed2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ coverage/ *.temp .direnv .env + +# Embedded repo (nested clone — not a submodule) +jam-nodes/ diff --git a/packages/nodes/src/integrations/dataforseo/seo-audit.ts b/packages/nodes/src/integrations/dataforseo/seo-audit.ts index d00a899..7611757 100644 --- a/packages/nodes/src/integrations/dataforseo/seo-audit.ts +++ b/packages/nodes/src/integrations/dataforseo/seo-audit.ts @@ -339,9 +339,12 @@ export const seoAuditNode = defineNode({ let isPassed: boolean; if (POSITIVE_CHECKS.has(checkId)) { + // Positive checks: passed=true means the good thing IS present (e.g. is_https) isPassed = checkData.passed; } else { - isPassed = checkData.passed; + // Normal checks: passed=true means the problem is ABSENT (i.e. the page is clean) + // DataForSEO sets passed=true to mean "no issue found", so invert for our tracking + isPassed = !checkData.passed; } if (isPassed) { diff --git a/packages/nodes/src/utils/http.ts b/packages/nodes/src/utils/http.ts index 702dcb6..e389c07 100644 --- a/packages/nodes/src/utils/http.ts +++ b/packages/nodes/src/utils/http.ts @@ -105,6 +105,14 @@ export async function fetchWithRetry( await sleep(delayMs); continue; } + + // All retries exhausted — throw instead of returning the 429 response + const errorText = await response.text(); + throw new FetchRetryError( + `Rate limit exceeded after ${maxRetries} attempts`, + 429, + errorText + ); } // Retry on server errors diff --git a/packages/nodes/src/utils/index.ts b/packages/nodes/src/utils/index.ts index 3064450..4115770 100644 --- a/packages/nodes/src/utils/index.ts +++ b/packages/nodes/src/utils/index.ts @@ -5,3 +5,8 @@ export { FetchRetryError, type FetchWithRetryOptions, } from './http.js'; + +export { + generateAnthropicText, + type AnthropicGenerateOptions, +} from './anthropic.js'; From db5cfc77eed6f11da54c7717a521544dc82bd425 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 28 Feb 2026 18:27:39 +0530 Subject: [PATCH 2/2] refactor(nodes): extract shared resolvePath utility, use in map - Add utils/resolve-path.ts as canonical path resolver - Supports dot notation, keyed array (contacts[0]), standalone index ([0].name), and empty paths - map.ts now imports from shared utility --- package-lock.json | 19 ++++++-- packages/nodes/src/transform/filter.ts | 23 +-------- packages/nodes/src/transform/map.ts | 29 +---------- packages/nodes/src/utils/index.ts | 5 +- packages/nodes/src/utils/resolve-path.ts | 61 ++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 57 deletions(-) create mode 100644 packages/nodes/src/utils/resolve-path.ts diff --git a/package-lock.json b/package-lock.json index 36b2561..888c928 100644 --- a/package-lock.json +++ b/package-lock.json @@ -925,6 +925,7 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1233,6 +1234,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1908,6 +1910,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -2247,6 +2250,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -2589,6 +2593,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -2613,6 +2618,7 @@ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -3208,6 +3214,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3429,6 +3436,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3441,6 +3449,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3454,6 +3463,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -4015,6 +4025,7 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -4121,6 +4132,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4364,6 +4376,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -4379,7 +4392,7 @@ }, "packages/core": { "name": "@jam-nodes/core", - "version": "0.2.4", + "version": "0.2.5", "license": "MIT", "dependencies": { "jsonpath-plus": "^10.0.0" @@ -4433,7 +4446,7 @@ }, "packages/nodes": { "name": "@jam-nodes/nodes", - "version": "0.2.4", + "version": "0.2.5", "license": "MIT", "dependencies": { "@jam-nodes/core": "^0.2.1" @@ -4448,7 +4461,7 @@ }, "packages/playground": { "name": "@jam-nodes/playground", - "version": "0.1.1", + "version": "0.1.2", "license": "MIT", "dependencies": { "@jam-nodes/core": "*", diff --git a/packages/nodes/src/transform/filter.ts b/packages/nodes/src/transform/filter.ts index cb3e7aa..24ae9a1 100644 --- a/packages/nodes/src/transform/filter.ts +++ b/packages/nodes/src/transform/filter.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { defineNode } from '@jam-nodes/core'; +import { resolvePath } from '../utils/resolve-path.js'; /** * Filter operator schema @@ -44,28 +45,6 @@ export const FilterOutputSchema = z.object({ export type FilterOutput = z.infer; -/** - * Resolve a nested path on an object - */ -function resolvePath(obj: unknown, path: string): unknown { - // Empty path means use the item itself - if (!path) { - return obj; - } - - const parts = path.split('.'); - let current: unknown = obj; - - for (const part of parts) { - if (current === null || current === undefined) { - return undefined; - } - current = (current as Record)[part]; - } - - return current; -} - /** * Evaluate filter condition */ diff --git a/packages/nodes/src/transform/map.ts b/packages/nodes/src/transform/map.ts index c169fad..37592ef 100644 --- a/packages/nodes/src/transform/map.ts +++ b/packages/nodes/src/transform/map.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { defineNode } from '@jam-nodes/core'; +import { resolvePath } from '../utils/resolve-path.js'; /** * Input schema for map node @@ -23,34 +24,6 @@ export const MapOutputSchema = z.object({ export type MapOutput = z.infer; -/** - * Resolve a nested path on an object - */ -function resolvePath(obj: unknown, path: string): unknown { - const parts = path.split('.'); - let current: unknown = obj; - - for (const part of parts) { - if (current === null || current === undefined) { - return undefined; - } - - // Handle array access like "[0]" - const arrayMatch = part.match(/^\[(\d+)\]$/); - if (arrayMatch) { - if (Array.isArray(current)) { - current = current[parseInt(arrayMatch[1]!, 10)]; - } else { - return undefined; - } - } else { - current = (current as Record)[part]; - } - } - - return current; -} - /** * Map node - extract a property from each item in an array. * diff --git a/packages/nodes/src/utils/index.ts b/packages/nodes/src/utils/index.ts index 4115770..9aa470e 100644 --- a/packages/nodes/src/utils/index.ts +++ b/packages/nodes/src/utils/index.ts @@ -6,7 +6,4 @@ export { type FetchWithRetryOptions, } from './http.js'; -export { - generateAnthropicText, - type AnthropicGenerateOptions, -} from './anthropic.js'; +export { resolvePath } from './resolve-path.js'; diff --git a/packages/nodes/src/utils/resolve-path.ts b/packages/nodes/src/utils/resolve-path.ts new file mode 100644 index 0000000..b4b4d7c --- /dev/null +++ b/packages/nodes/src/utils/resolve-path.ts @@ -0,0 +1,61 @@ +/** + * Shared path resolution utility for transform nodes. + * + * Resolves a dot-notation path string against an arbitrary object. + * This mirrors the same logic as `ExecutionContext.resolveNestedPath`, + * but operates on any object rather than the workflow variable store. + * + * Supported syntax: + * - Dot notation: "contact.email" + * - Keyed array access: "contacts[0].name" + * - Standalone array index: "[0].name" (when current value is an array) + * - Empty path returns the object itself + * + * @example + * resolvePath({ a: { b: [1, 2] } }, 'a.b[1]') // → 2 + * resolvePath([{ id: 1 }, { id: 2 }], '[0].id') // → 1 + * resolvePath({ score: 42 }, '') // → { score: 42 } + */ +export function resolvePath(obj: unknown, path: string): unknown { + // Empty path means use the value itself + if (!path) { + return obj; + } + + const parts = path.split('.'); + let current: unknown = obj; + + for (const part of parts) { + if (current === null || current === undefined) { + return undefined; + } + + // Handle keyed array access like "contacts[0]" + const keyedArrayMatch = part.match(/^(\w+)\[(\d+)\]$/); + if (keyedArrayMatch) { + const [, key, index] = keyedArrayMatch; + current = (current as Record)[key!]; + if (Array.isArray(current)) { + current = current[parseInt(index!, 10)]; + } else { + return undefined; + } + continue; + } + + // Handle standalone array index like "[0]" + const standaloneIndexMatch = part.match(/^\[(\d+)\]$/); + if (standaloneIndexMatch) { + if (Array.isArray(current)) { + current = current[parseInt(standaloneIndexMatch[1]!, 10)]; + } else { + return undefined; + } + continue; + } + + current = (current as Record)[part]; + } + + return current; +}